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