Przenoszenie sprawdzeń z procesów iteracyjnych

Załóżmy, że utknęliśmy w naszym procesie optymalizacji i nie wiemy, co powinniśmy teraz zmienić. Co powinniśmy zrobić? Cóż, jak wspomnieliśmy wcześniej, powinniśmy sprofilować nasz kod, aby poznać nasze obecne wąskie gardła i to właśnie robimy tutaj. Używamy tej funkcji Rprof() ponownie do profilowania naszej implementacji sma_slow_5(). Wyniki pokazują, że funkcje [.data.frame i [  są naszymi największymi wąskimi gardłami i chociaż ich nazwy są nieco tajemnicze, możemy się domyślić, że są one związane z podziałem ramek danych (którymi są). Oznacza to, że naszym obecnie najważniejszym wąskim gardłem jest sprawdzenie, czy znajdujemy się w obserwacji, która odpowiada używanej przez nas obserwacji, i wykonujemy takie sprawdzenia w różnych miejscach w naszym kodzie:

Rpof()

sma_5 <- sma_slow_5(period, symbol, data_original[1:10000, ] )

Rprof (NULL}

summaryRprof()

#> $ by.self

#> self.time self.pct total.time total.pct

#> „[.data.frame” 0,54 26,21 1,24 60,19

#> “[” 0,22 10,68 1,34 65,05

#> „NextMethod” 0,20 9,71 0,20 9,71

#> “sma_slow_5” 0,14 6,80 2,06 100,00

#> “Ops.factor” 0,12 5,83 0,52 25,24

#> (Obcięte wyjście)

#>

#> $ by.total

#> total.time total.pct self.time self.pct

#> “sma_slow_5” 2,06 100,00 0,14 6,80

#> “[” 1,34 65,05 0,22 10,68

#> “[.data.frame” 1,24 60,19 0,54 26,21

#> „Ops.factor” 0,52 25,24 0,12 5,83

#> „NextMethod” 0,20 9,71 0,20 9,71

#> (Obcięte wyjście)

Teraz, gdy znamy nasze obecne największe wąskie gardło, możemy je usunąć, unikając sprawdzania, czy bieżąca obserwacja odpowiada symbol, którą otrzymujemy jako parametr. Aby to osiągnąć, po prostu wprowadzamy filtr na początku funkcji, który przechowuje tylko obserwacje zawierające prawidłowy symbol. Zwróć uwagę, że ten prosty filtr pozwala nam usunąć dwie kontrole, które wykonywaliśmy wcześniej, ponieważ jesteśmy pewni, że wszystkie obserwacje mają poprawny symbol. Zmniejsza to dwa poziomy wcięć w naszym kodzie, ponieważ te sprawdzenia zostały zagnieżdżone. To wspaniałe uczucie, prawda? Teraz wydaje się, że mamy bardzo prostą implementację, która intuicyjnie będzie działać znacznie lepiej. Aby to zweryfikować, przechodzimy do benchmarku i sprawdzamy poprawność, jak wcześniej:

performance <- microbenchmark {

sma+6 <- sma_slow_6(perid, symbol, data),

unit =”us”

)

all(sma_1$sma – sma_6 <- 0.001, na.rm = TRUE)

#> TRUE

summary(performance)$median

#> [1] 2991.5720

Potwierdza się również nasza intuicja; nasz średni czas dla sma_slow_6()  to 2, 991.57. To jest tylko 17% w porównaniu z wcześniejszą najszybszą implementacją, którą mieliśmy dla sma_slow_5() , i zajmuje to tylko 3% czasu, jaki zajmowała nasza pierwsza implementacja. Czy to jest niesamowite, czy co? Spójrz na poniższą tabelę:

 

Usunięcie niepotrzebnej logiki

Są chwile, kiedy prosta logika pokazuje nam, że są części naszych implementacji, które są niepotrzebne. W tym konkretnym przypadku kumulacji period_prices można uniknąć, ustawiając sma na 0 początkowo zamiast NA i dodając do niej każdą cenę. Jednak robiąc to, tracimy kontrolę nad liczbą elementów w wektorze, więc funkcja mean() nie ma już sensu i przystępujemy do podzielenia sumy przez period tak, jak robiliśmy to wcześniej:

sma_slow_5 <- function(period, symbol, data) {

result <- NULL

for(end in 1:nrow(data)) {

position <- end

sma <- 0

n_acumulated <- 0

if (data[end , „symbol”] = = symbol) {

while(n_acumulated < period & position >- 1) {

if (data[position, „symbol”] = = symbol) {

sma <- sma + data[position, „price_usd”]

n_accumulated <- n_accumulated + 1

}

position <- position -1

}

if (n_acumulated == period) {

sma <- sma / period

} else {

sma <- NA

}

result <- c(result, sma)

}

}

return(reuslt)

}

Ponownie porównujemy i sprawdzamy poprawność, jak pokazano w poniższym fragmencie kodu:

performance <- microbenchmark(

sma_5 <- sma_slow_5(period, symbol, data),

unit = „us”

)

all(sma_1$sma – sma_5 <= 0.001, na.rm = TRUE)

#> TRUE

summary(performance)$median

#> [1] 16682.68

W tym przypadku nasz średni czas wyniósł 16682.68 mikrosekundy, co sprawia, że jak dotąd jest to najszybsza implementacja. Ponownie zwróć uwagę, że bardzo prosta zmiana spowodowała redukcję o około 17% w porównaniu z wcześniejszą najszybszą implementacją:

Wektoryzacja tak bardzo, jak to możliwe

Wektoryzacja oznacza usunięcie ręcznego mechanizmu zapętlania na korzyść operacji zoptymalizowanej do zrobienia tego samego bez potrzeby jawnej pętli. Jest to bardzo pomocne, ponieważ pomaga uniknąć narzutu związanego z jawnymi pętlami w R. Wektoryzacja jest podstawowym narzędziem w języku R i należy przyzwyczaić się do programowania przy użyciu go zamiast używania jawnych pętli, gdy tylko jest to możliwe, bez czekania, aż nadejdzie etap wydajności do gry. Kiedy zrozumiesz, jak to działa, przyjdzie naturalnie. Istnieją różne sposoby wektoryzacji operacji. Na przykład, jeśli chcesz wykonać mnożenie macierzy wektorów, zamiast iterować po elementach wektora i macierzy, mnożąc odpowiednie współczynniki i dodając je razem, jak to zwykle robi się w innych językach programowania, możesz po prostu zrobić coś w rodzaju A %*% A aby wykonać wszystkie te operacje w sposób zwektoryzowany w R. Wektoryzacja zapewnia bardziej wyrazisty kod, który jest łatwiejszy do zrozumienia i wydajniejszy, dlatego zawsze należy próbować go używać. Innym sposobem wektoryzacji jest użycie rodziny funkcji apply() R (na przykład, lapply() ,sapply() i tak dalej). Spowoduje to wygenerowanie prostszego kodu niż jawne pętle, a także przyspieszy implementację. W rzeczywistości funkcja apply() jest przypadkiem szczególnym, ponieważ nie jest tak zoptymalizowana, jak inne funkcje z jej rodziny, więc wzrost wydajności nie będzie tak duży, jak w przypadku innych funkcji, ale przejrzystość kodu rzeczywiście wzrośnie. Innym sposobem wektoryzacji kodu jest zastąpienie pętli wbudowanymi funkcjami języka R i tak właśnie będzie w następnej modyfikacji. W trzecim if w kodzie, tym po pętla while się skończyła, istnieje pętla for, która dodaje elementy, które mamy w wektorze period_prices, a następnie jest dzielona przez wektor period, aby uzyskać średnią. Możemy po prostu użyć funkcji mean() zamiast korzystać z takiej pętli i to właśnie robimy. Teraz, kiedy czytasz tę część kodu, łatwo odczytuje się, jakby liczba skumulowanych cen była równa okresowi, co sprawia, że ​​SMA jest równe średniej skumulowanych cen. Znacznie łatwiej jest zrozumieć kod niż użycie pętli:

sma_slow_4 <- function(period, symbol, data) {

resultl <- NULL

for(end in 1:nrow(data) ) {

position <- end

sma <- NA

n_accumulated <- 0

period_proces <- NULL

if (data[end, „symbol”] == symbol) {

while(n_acumulated < eriod & position >- 1) {

if (data[position, „symbol”] = = symbol) {

priod_prices <- c(period_prices,

data[position, „price_isd”])

n_acumulated <- n_accumulated + 1

}

position <- position -1

}

if (n_aumulated = = period) {

sma <- mean(period_prices)

} else {

sma <- NA

result <- c(result, sma)

}

}

return(result)

}

Ponownie porównujemy i sprawdzamy poprawność. Jednak w tym przypadku okazuje się, że średni czas to 20,825.879 mikrosekundy, czyli więcej niż bieżące minimum od sma_slow_3(). Czy kod wektorowy nie powinien być szybszy? Odpowiedź jest taka, że ​​przez większość czasu tak jest, ale w takich sytuacjach funkcja mean() ma narzut, ponieważ musi sprawdzić, z jakim typem obiektu ma do czynienia, przed użyciem jej do jakichkolwiek operacji, co może spowalniają implementację. Kiedy używaliśmy jawnej pętli, sumy i dzielenie były znacznie niższe, ponieważ można je było zastosować do znacznie mniejszego zestawu obiektów. Dlatego, jak widać w poniższej tabeli, sma_slow_4() zajmuje o 6% więcej czasu niż sma_slow_3(). To niewiele, a ponieważ wolę kod ekspresyjny, zachowam zmianę:

performance <- mirobenchmark (

sma_4 <- sma_slow_4 (period, symbol ,data) ,

unit =”us”

)

all(sma_1$sma – sma_4 <= 0.001 , na.rm = TRUE)

#> TRUE

 summary(performance)$median

#> [1] 20825.8790

Spójrz na poniższą tabelę:

Jeśli chcesz porównać narzut funkcji mean() z narzutem wynikającym z innych sposobów wykonywania tych samych obliczeń, spójrz na poniższy punkt odniesienia. Funkcja .Internal(mean(x)) omija mechanizm wysyłania metod, które pokazaliśmy w poprzednim rozdziale i przeskakuje bezpośrednio do implementacji funkcji mean() w C, jak pokazano w poniższym fragmencie kodu:

x <- sample(100)

performance <- microbenchmark (

mean(x),

sum(x) / length(x),

.Internal(mean(x)),

Times = 1e+05

)

performance

#> Jednostka: nanosekundy

#> wyr min lq średnia mediana uq max neval

#> średnia (x) 1518 1797 2238,2607 1987 2230 2335285 1e + 05

#> suma (x) / długość (x) 291345 750.2324 403488 27016544 1e + 05

#>. Wewnętrzna (średnia (x)) 138153 187.0588 160176 34513 1e + 05

Łatwe osiąganie wysokich korzyści – poprawa kosztów

W tej sekcji pokażemy, jak można radykalnie poprawić wydajność języka R bez uciekania się do zaawansowanych technik, takich jak delegowanie do innych języków programowania lub wdrażanie równoległości. Techniki te zostaną pokazane w dalszych sekcjach.

Korzystanie z prostej struktury danych dla zadania

Wielu użytkowników języka R zgodziłoby się, że ramka danych jako struktura danych jest podstawowym narzędziem do analizy danych. Zapewnia intuicyjny sposób reprezentowania typowego ustrukturyzowanego zbioru danych z wierszami i kolumnami reprezentującymi odpowiednio obserwacje i zmienne, ale zapewnia większą elastyczność niż macierz, umożliwiając zmienne różnych typów (takie jak zmienne znakowe i numeryczne w jednej strukturze). Ponadto, gdy ramki danych zawierają tylko zmienne numeryczne, podstawowe operacje macierzowe można wygodnie zastosować do nich bez konieczności jawnego wymuszania. Ta wygoda wiąże się jednak z kosztami wydajności, o których ludzie często nie wspominają. Tutaj unikamy powtarzania wyników Rprof(), które otrzymaliśmy z profilowania funkcji sma_slow)1(). Jeśli jednak spojrzysz na nie, zobaczysz  ,że rbind() i datat.frame() należą one do funkcji, które zajmowały najwięcej czasu. To jest właśnie wspomniany wcześniej koszt wydajności. Jeśli chcesz, aby implementacje były szybsze, unikanie ramek danych może być dobrym początkiem. Ramki danych mogą być doskonałym narzędziem do analizy danych, ale nie podczas pisania szybkiego kodu. Jak widać w sma_slow_2(), kod jest praktycznie taki sam jak sma_slow_1(), z wyjątkiem tego, że obiekt period_prices() nie jest już ramką danych. Zamiast tego stał się wektorem, który jest rozszerzany funkcją c() zamiast funkcji rbind(). Zwróć uwagę, że wciąż dynamicznie zwiększamy rozmiar obiektu podczas wywoływania funkcji c(), czego nie powinieneś robić dla wykonującej ody, ale zrobimy to krok po kroku:

sma_slow_2 <- funtion(period, symbol, dat) {

result  <- datat.frame(sma = numeric () )

for(end i 1:nrow(data)) {

position <- end

sma <- NA

n_acumulated <- 0

period_prices <- NULL

if(data[end, „symbol”] = = symbol) {

while(n_accumulated < period & position >= 1) {

if (data[position, „symbol”] = = symbol) {

period_prices <- c(period_prices,

data[position, „price_us:])

n_acumulated <- n_acumulated + 1

}

position <- position – 1

}

if (n_acumulated = = period) {

sma <- 0

for (price in period_prices) {

sma <- sma + price

}

sma <- sma / period

} else {

sma <- NA

}

result <- rbind(result, data.frame(sma=sma))

}

}

return(result)

}

W tym przypadku czas jego wykonania mierzymy tak jak wcześniej, ale wykonujemy też bardzo ważną weryfikację, która jest często pomijana. Weryfikujemy, że wartości, które otrzymujemy z sma_slow_1(), są takie same, jak te, które otrzymujemy z sma_slow_2() . Nie byłoby poprawnym porównaniem, gdybyśmy mierzyli implementacje, które robią różne rzeczy. Wykonanie sprawdzenia jest również przydatne, aby zwiększyć naszą pewność, że każda wprowadzana przez nas zmiana nie powoduje nieoczekiwanego zachowania. Jak widać, wszystkie wartości są takie same, więc możemy kontynuować z pewnością:

performance <- microbenchmark (

sma_2 <- sma_slow_2(period, symbol, data),

unit = „us”

)

all(sma_1$sma ==sma_2$sma, na.rm = TRUE_

>#> TRUE

summary(performance)$median

#> [1] 33031.7785

Rejestrujemy nasze wyniki w naszej tabeli i zdajemy sobie sprawę, że usunięcie tej struktury ramki danych pozwoliło nam usunąć dwie trzecie czasu wykonywania. To całkiem nieźle jak na tak łatwą zmianę, prawda? Ponieważ nasz podstawowy przypadek (najszybsza implementacja, jaką mamy do tej pory) to sma_slow_2(), widzimy, że sma_slow_1() wykonanie zajmie około 145% więcej czasu:

Teraz, gdy zdajemy sobie sprawę, jaki wpływ mogą mieć niepotrzebne ramki danych na działanie naszego kodu, przystępujemy do usuwania również drugiej ramki danych, której używaliśmy dla obiektu result. Zastępujemy go również wektorem i używamy funkcji c(), aby do niego dołączyć. Ten sam problem dynamicznej ekspansji, o którym mowa wcześniej, pojawia się również tutaj. Jak widać, wszystko inne jest takie samo. Przechodzimy do testu porównawczego, tak jak robiliśmy to wcześniej, i sprawdzamy również, czy otrzymane wyniki są takie same. Ostrożny czytelnik mógł zauważyć, że poprzednia kontrola została przeprowadzona z operatorem równości, podczas gdy ta została przeprowadzona z operatorem nierówności. W rzeczywistości, sprawdzając liczby rzeczywiste, lepiej jest sprawdzić, czy są one wystarczająco zbliżone, a nie dokładnie takie same. Jeśli sprawdziłeś identyczne liczby, możesz otrzymać wynik FALSE, ponieważ jedna z liczb ma różnicę 0.000000001, która w naszym przypadku nie jest istotna. Dlatego ustalamy, co jest istotnym sprawdzianem dla naszego konkretnego przypadku użycia i sprawdzamy, czy każda para liczb ma różnicę nie większą niż ten próg, tak jak robimy tutaj, z naszym progiem 0.001:

performance <- microbenchmark(

sma_3 <- sma_slow_3 (period, symbol, data),

unit = „us”

)

all(sma_1$sma- sma_3 <= 0.001,na.rm = TRUE)

#> TRUE

summary(performance)$mediaan

#> [1] 19628.243

W tym przypadku średni czas potrzebny do wykonaniasma_slow_3() wyniósł 19, 628.243 mikrosekundy. Kontynuujemy i zapisujemy to w naszej tabeli i ponownie obliczamy procent od najlepszego, który jest sma_slow_3() w tym momencie. Zauważ, że jesteśmy w stanie usunąć prawie połowę czasu z już ulepszonej funkcji sma_slow_2(), a użycie oryginalnej funkcji sma_slow_1() zajmie 312% więcej czasu niż najnowsza. Zaskakujące może być to, jak duży wzrost wydajności można uzyskać, używając prostszej struktury danych

Automatyczna analiza porównawcza za pomocą microbenchmark()

Jeśli zidentyfikowałeś funkcję, która jest wielokrotnie wywoływana w Twoim kodzie i wymaga przyspieszenia, możesz napisać dla niej kilka implementacji i użyć funkcji micrbenchmark() z pakietu microbenchmark, aby je porównać. Jego wyniki będą również zwykle bardziej wiarygodne, ponieważ domyślnie uruchamia każdą funkcję 100 razy, dzięki czemu jest w stanie wygenerować statystyki dotyczące jej wydajności. Aby użyć tej funkcji, po prostu zawiń ją wokół fragmentu kodu, który chcesz zmierzyć. Niektóre przydatne funkcje polegają na tym, że możesz wykonać zadanie, w ramach którego bardzo przydatne jest zmierzenie i wykorzystanie wyników za jednym razem; możesz także przekazywać różne wywołania funkcji oddzielone przecinkami, a to da ci wyniki dla każdego z nich. W ten sposób można jednocześnie automatycznie porównywać różne funkcje. Tutaj przypiszemy wyniki sma_slow_1() do sma_1, tak jak poprzednio, ale od tego czasu jest opakowany w funkcję microbenchmark(), zostanie również zmierzony, a wyniki wydajności zostaną zapisane w ramce danych performance. Ten obiekt zawiera następujące kolumny: exprneval to ciąg znaków, który zawiera użyte wywołanie funkcji, to ile razy funkcja została wykonana (domyślnie jest to 100) oraz min,lq (pierwszy kwartyl),mean,median,uq (trzeci kwartyl) i statystyki max:

performance <- microbenchmark (

sma_1 <- sma_slow_1(period, symbl, data),

unit = „us”

)

summary(performance)#median

#> [1] 81035.19

Jeśli chcesz zobaczyć pełną ramkę danych wydajności, po prostu ją wydrukuj. Tutaj pokazaliśmy tylko, że czas median potrzebny do wykonania wywołania funkcji sma_slow_1()  wynosił  81, 035.19 mikrosekundy (czyli jednostkę określoną w parametrze unit=”us”). Domyślnie zajęłoby to milisekundy zamiast mikrosekund, ale chcemy zapewnić te same jednostki dla wszystkich porównań, które wykonujemy  i mikrosekund to lepsza opcja. Będziemy nadal dodawać rekordy do poniższej tabeli. Każdy wiersz będzie zawierał identyfikator implementacji, medianę w mikrosekundach potrzebnych do wykonania takiej funkcji, wskazanie najszybszej jak dotąd implementacji oraz procent w porównaniu z najszybszą, jaką mamy do tej pory. W tym konkretnym przypadku, ponieważ jest to jedyne, które zrobiliśmy, jest to oczywiście najszybsze, a także w 100% najlepsze, które samo w sobie jest:

Celem pozostałej części  jest rozszerzenie tej tabeli, aby zapewnić precyzyjne pomiary tego, jak wiele ulepszeń wydajności wprowadzamy w miarę ulepszania naszej implementacji SMA.

Benchmarking ręczny za pomocą system.time ()

Teraz przyjrzymy się, jak przetestować Twój kod. Jeśli szukasz prostego pomiaru czasu realizacji, to syste.time to dobry wybór. Po prostu wywołujesz w nim funkcję, która wydrukuje dla Ciebie następujące trzy miary czasu:.

  • user : Jest to czas user, na który powinniśmy zwrócić większą uwagę, ponieważ mierzy on czas procesora używany przez R do wykonania kodu
  • system : Czas system jest miarą czasu spędzonego przez system, aby móc wykonać funkcję
  • elapsed : Czas elapsed to całkowity czas potrzebny na wykonanie kodu, nawet jeśli został spowolniony z powodu innych procesów systemowych

Czasami czas elapsed jest dłuższy niż suma czasu user i czasu system, ponieważ procesor wykonuje wiele zadań jednocześnie z innymi procesami lub musi czekać na udostępnienie zasobów, takich jak pliki i połączenia sieciowe. W innych przypadkach czas, który upłynął, jest krótszy niż suma czasu i czasu. Może się tak zdarzyć, gdy do wykonania wyrażenia jest używanych wiele wątków lub procesorów. Na przykład zadanie, które zajmuje 10 sekund czasu użytkownika, może zostać wykonane w ciągu 5 sekund, jeśli obciążenie dzielą dwa procesory. Najczęściej jednak interesuje nas całkowity czas, jaki upłynął do wykonania danego wyrażenia. Gdy wyrażenie jest wykonywane w pojedynczym wątku (wartość domyślna dla języka R), upływający czas jest zwykle bardzo zbliżony do sumy czasu i czasu. Jeśli tak nie jest, albo wyrażenie spędziło czas, czekając na dostępność zasobów, albo w systemie było wiele innych procesów konkurujących o czas procesora. W każdym razie, jeśli podejrzewasz swoje pomiary, spróbuj mierzyć ten sam kod kilka razy, podczas gdy komputer nie zużywa zasobów na inne aplikacje. W tym konkretnym przypadku widzimy, że wykonanie zajęło około 9 sekund, aby zakończyć, co w przybliżeniu odpowiada temu samemu czasowi, jaki zajęło wykonanie go, mierząc Rprof() w poprzedniej sekcji, jak widać w kolumnie total.time w sma_slow_1() dotyczącej obserwacji tabeli $by.total.

system.time(sma_slow_1 (period, symbol. data_original[1:10000, ]))

#> user system elapsed

#> 9.251 0.015 9.27

Jeśli chcesz zmierzyć wiele funkcji, aby porównać ich czasy, będziesz musiał użyć funkcji system.time ()na każdej z nich, więc jest to trochę ręczny proces. Lepszą alternatywą jest funkcja microbenchmark.

Podstawy profilowania z Rprof()

Nawet doświadczonym programistom trudno jest zidentyfikować wąskie gardła w swoim kodzie. Jeśli nie masz sporego doświadczenia i dobrego wyczucia, które części twojego kodu spowalniają jego wykonanie, prawdopodobnie lepiej będzie profilować swój kod, zanim zaczniesz go optymalizować. Dopiero po zidentyfikowaniu najważniejszych wąskich gardeł możesz podjąć próbę ich wyeliminowania. Trudno jest podać ogólne porady dotyczące poprawy wydajności, ponieważ każda implementacja jest zupełnie inna. Funkcja RProf() jest wbudowanym narzędziem do profilowania wykonywania funkcji języka R. W regularnych odstępach czasu profiler zatrzymuje interpreter, zapisuje bieżący stos wywołań funkcji i zapisuje informacje do pliku. Następnie możemy spojrzeć na podsumowania takich informacji, aby dowiedzieć się, gdzie nasza implementacja spędza najwięcej czasu. Pamiętaj, że wyniki z Rprof() są stochastyczne. Za każdym razem, gdy go użyjemy, wyniki będą nieco inne, w zależności od wielu rzeczy specyficznych dla twojego systemu, na które R nie ma wpływu. Dlatego wyniki, które otrzymujemy z Rprof(), są szacunkowe i mogą się różnić w ramach przykładowej realizacji. Aby użyć tej funkcji, po prostu wywołujemy ją bez parametrów, zanim wywołamy kod, który chcemy zmierzyć, a następnie wywołujemy ją ponownie, tym razem wysyłając parametr NULL. Wyniki są zapisywane w pliku na dysku twardym i można je wywołać za pomocą wywołania funkcji summaryRprof().

W tym konkretnym przypadku zwróć uwagę, że wysłaliśmy pierwsze 10000 elementów. Gdybyśmy wysłali niewielką ilość danych, funkcja sma_slwo_1() zakończyłaby się tak szybko, że nie mielibyśmy żadnego sensownego wyniku (pamiętaj, że Rprof() mierzy w odstępach czasu). Pokazane tutaj wyniki są również obcięte, ponieważ rzeczywiste wyniki są znacznie większe, ponieważ pokazują wiele wywołań funkcji, których użył nasz kod. Pozostawiliśmy pięć najlepszych wyników dla każdej tabeli. Obie tabele zawierają te same informacje. Różnica polega na tym, że tabela $by.self (pierwsza) jest uporządkowana według self, a tabela $by.total (druga) według total; self wskazuje, ile czasu zajęło wywołanie funkcji bez uwzględnienia wywołań funkcji potomnych, podczas gdy total informacje obejmują wywołania funkcji potomnych. Oznacza to, że dane muszą sumować się, podczas gdy dane zagregowane sumują się zwykle do znacznie większe niż 100:

Rprof()

sma_1 <- sma_slow_1 (period, symbol, data_original[1:10000, ])

Prof.(NULL)

summaryRprof()

#> $ by.self

#> self.time self.pct total.time total.pct

#> “rbind” 1,06 10,84 6,16 62,99

#> “struktura” 0,82 8,38 0,94 9,61

#> “data.frame” 0,68 6,95 4,32 44,17

#> “[.data.frame” 0,54 5,52 1,76 18,00

#> “sma_slow_1” 0,48 4,91 9,78 100,00

#> (Obcięte wyjście)

#>

#> $ by.total

#> total.time total.pct self.time self.pct

#> “sma_slow_1” 9,78 100,00 0,48 4,91

#> “rbind” 6,16 62,99 1,06 10,84

#> “data.frame” 4,32 44,17 0,68 6,95

#> “[” 1,88 19,22 0,20 2,04

#> “as.data.frame” 1,86 19,02 0,10 1,02

#> (Obcięte wyjście)

#>

#> $ sample.interval

#> [1] 0,02

#>

#> $ sampling.time

#> [1] 9.78

Jak widać w wynikach, pierwsza kolumna wskazuje wywołanie funkcji na stosie, a liczby wskazują, ile czasu zostało spędzone na określonym wywołaniu funkcji, w kategoriach bezwzględnych (time) lub względnych (pct). Zwykle będziesz chciał skupić się na najwyższych wartościach w kolumnie self.pct tabeli $by.self, ponieważ pokazują one funkcje, które same zajmują najwięcej czasu. W tym konkretnym przypadku rbind,structure, i data.frame są funkcjami, które zajmują najwięcej czasu. Na koniec powinieneś wiedzieć, że niektóre nazwy znalezione w stosie wywołań funkcji mogą być bardzo tajemnicze i czasami będziesz miał trudności ze znalezieniem odnośników lub dokumentacji do nich. Dzieje się tak, ponieważ są to prawdopodobnie wewnętrzne implementacje języka R, które nie są przeznaczone do bezpośredniego użytku przez użytkowników języka R. Sugeruję, aby po prostu spróbować naprawić te wywołania funkcji, które rozpoznajesz, chyba że masz do czynienia z sytuacjami, w których wysoce zoptymalizowany kod jest absolutnym wymogiem, ale w takim przypadku lepiej byłoby przeczytać specjalistyczną książkę na ten temat.

Pomiar poprzez profilowanie i analizę porównawczą

Istnieje powszechne powiedzenie, które mówi, że nie można zmienić tego, czego nie można zmierzyć. Nawet jeśli technicznie możesz zmienić wydajność swojego kodu w R, na pewno nie będziesz w stanie wiedzieć, czy zmiana jest tego warta, jeśli jej nie zmierzysz. W tej sekcji przedstawimy trzy narzędzia, których możesz użyć do pomiaru kodu: Rprof(), system.time i microbenchmark(). Pierwsze dwa są zawarte w R, a trzeci wymaga zainstalowania pakietu microbenchmark. Narzędzie Rprof() służy do profilowania kodu, natomiast system.time() i microbenchmark() służy do testowania kodu. *Profilowanie oznacza, że mierzysz, ile czasu dana implementacja spędza na każdej jej części. *Benchmarking oznacza, że porównujesz łączny czas potrzebny na wykonanie różnych implementacji, aby porównać je między sobą, bez względu na ich wewnętrzne części.

Procesy jednowątkowe

Czwartym najważniejszym wąskim gardłem, z którym spotykają się ludzie, jest fakt, że język R nie ma wyraźnych konstrukcji dla paralelizmu. Natychmiastowa instalacja R nie może korzystać z wielu procesorów i nie ma znaczenia, czy zainstalujesz R na potężnym serwerze z 64 rdzeniami procesora, R użyje tylko jednego z nich. Sposobem na rozwiązanie tego problemu jest wprowadzenie równoległości w implementacjach. Jednak zrobienie tego wcale nie jest łatwym zadaniem. W rzeczywistości poważne wysiłki związane z równoległością wymagają głębokiej wiedzy o sprzęcie i oprogramowaniu i często zależą od konkretnego sprzętu używanego do wykonania implementacji. Chociaż jest to bardzo trudne, a może nawet z tego powodu, R ma wiele pakietów, których celem jest zapewnienie równoległych rozwiązań dla określonych funkcji języka R. Istnieje kilka ogólnych pakietów, których możesz użyć do tworzenia własnych równoległych implementacji, co zobaczymy w dalszej części tego rozdziału, ale zdecydowanie nie jest to pierwsze miejsce, w którym należy szukać ulepszeń wydajności. Teraz, gdy rozumiesz, dlaczego R może być powolny, wykorzystamy tę wiedzę do stopniowego ulepszania implementacji SMA, którą pokazaliśmy wcześniej, ale zanim to zrobimy, musimy nauczyć się mierzyć wydajność naszego kodu i na tym skupiamy się w następnej sekcji.

Procesy związane z pamięcią

Trzecim najważniejszym wąskim gardłem, które ludzie odkrywają, jest to, że R musi mieć wszystkie obiekty w pamięci. Oznacza to, że komputer używany do analizy musi mieć wystarczającą ilość pamięci RAM, aby pomieścić jednocześnie wszystkie dane, a także obiekty pośrednie i wynikowe, i należy pamiętać, że ta pamięć RAM jest współdzielona z wszystkimi innymi aplikacjami działającymi na komputerze. Jeśli R nie ma wystarczającej ilości pamięci RAM, aby pomieścić każdy obiekt w pamięci, system operacyjny wykona operację zamiany, która w R będzie wyglądać tak, jakbyś miał wszystkie dane w pamięci, ale dane zostaną zapisane i odczytane z dysku twardego w rzeczywistość. Czytanie i pisanie z dysków twardych jest o rząd wielkości wolniejsze niż wykonywanie równoważnych operacji w pamięci, a R nie poinformuje Cię, że tak się dzieje, ponieważ naprawdę nie może (robi to system operacyjny). Aby wykryć, że tak się dzieje, należy zwrócić uwagę na narzędzie dostarczane przez system operacyjny do monitorowania zasobów systemu. Mimo że jest to trzecie wąskie gardło na liście, kiedy się zdarza, jest zdecydowanie najbardziej szkodliwe, ponieważ mamy wąskie gardło wejścia / wyjścia dysku na szczycie wąskiego gardła pamięci. Kiedy napotkasz ten problem, będziesz w stanie stwierdzić, ponieważ R wydaje się zawieszać lub nie reagować. Jeśli to ci się przytrafia, zdecydowanie powinieneś poszukać sposobów, aby to wyeliminować. Jest to trzecie miejsce na liście, ponieważ nie występuje tak często jak poprzednie dwa, a nie dlatego, że ma mniejszy wpływ.