Mam zamiar sprzeciwić się ogólnej mądrości, która std::copy
spowoduje niewielką, prawie niezauważalną utratę wydajności. Właśnie wykonałem test i stwierdziłem, że to nieprawda: zauważyłem różnicę w wydajności. Jednak zwycięzcą był std::copy
.
Napisałem implementację C ++ SHA-2. W moim teście haszuję 5 ciągów przy użyciu wszystkich czterech wersji SHA-2 (224, 256, 384, 512) i wykonuję pętlę 300 razy. Czasy mierzę za pomocą Boost.timer. Ten licznik pętli 300 wystarczy, aby całkowicie ustabilizować moje wyniki. Przeprowadzałem test 5 razy, na przemian z memcpy
wersją i std::copy
wersją. Mój kod korzysta z przechwytywania danych w jak największej liczbie fragmentów (wiele innych implementacji działa z char
/ char *
, podczas gdy ja operuję z T
/ T *
(gdzie T
jest największym typem w implementacji użytkownika, który ma prawidłowe zachowanie przepełnienia), więc szybki dostęp do pamięci na największe typy, jakie mogę, mają kluczowe znaczenie dla wydajności mojego algorytmu. Oto moje wyniki:
Czas (w sekundach) do ukończenia testów SHA-2
std::copy memcpy % increase
6.11 6.29 2.86%
6.09 6.28 3.03%
6.10 6.29 3.02%
6.08 6.27 3.03%
6.08 6.27 3.03%
Całkowity średni wzrost szybkości std :: copy over memcpy: 2,99%
Mój kompilator to gcc 4.6.3 w Fedorze 16 x86_64. Moje flagi optymalizacji to -Ofast -march=native -funsafe-loop-optimizations
.
Kod dla moich implementacji SHA-2.
Postanowiłem również przeprowadzić test na mojej implementacji MD5. Wyniki były znacznie mniej stabilne, więc zdecydowałem się zrobić 10 biegów. Jednak po kilku pierwszych próbach otrzymałem wyniki, które różniły się znacznie od jednego uruchomienia do drugiego, więc domyślam się, że miała miejsce jakaś aktywność systemu operacyjnego. Postanowiłem zacząć od nowa.
Te same ustawienia i flagi kompilatora. Jest tylko jedna wersja MD5 i jest szybsza niż SHA-2, więc zrobiłem 3000 pętli na podobnym zestawie 5 ciągów testowych.
Oto moje 10 ostatnich wyników:
Czas (w sekundach) do ukończenia testów MD5
std::copy memcpy % difference
5.52 5.56 +0.72%
5.56 5.55 -0.18%
5.57 5.53 -0.72%
5.57 5.52 -0.91%
5.56 5.57 +0.18%
5.56 5.57 +0.18%
5.56 5.53 -0.54%
5.53 5.57 +0.72%
5.59 5.57 -0.36%
5.57 5.56 -0.18%
Całkowity średni spadek prędkości std :: copy over memcpy: 0,11%
Kod mojej implementacji MD5
Te wyniki sugerują, że istnieje pewna optymalizacja, którą std :: copy wykorzystałem w moich testach SHA-2, std::copy
której nie można było użyć w moich testach MD5. W testach SHA-2 obie tablice zostały utworzone w tej samej funkcji, która wywołała std::copy
/ memcpy
. W moich testach MD5 jedna z tablic została przekazana do funkcji jako parametr funkcji.
Zrobiłem trochę więcej testów, aby zobaczyć, co mogę zrobić, aby std::copy
znowu przyspieszyć. Odpowiedź okazała się prosta: włącz optymalizację czasu łącza. Oto moje wyniki z włączonym LTO (opcja -flto w gcc):
Czas (w sekundach) do zakończenia wykonywania testów MD5 z opcją -flto
std::copy memcpy % difference
5.54 5.57 +0.54%
5.50 5.53 +0.54%
5.54 5.58 +0.72%
5.50 5.57 +1.26%
5.54 5.58 +0.72%
5.54 5.57 +0.54%
5.54 5.56 +0.36%
5.54 5.58 +0.72%
5.51 5.58 +1.25%
5.54 5.57 +0.54%
Całkowity średni wzrost szybkości std :: kopia nad memcpy: 0,72%
Podsumowując, nie wydaje się, aby korzystanie z niego miało wpływ na wydajność std::copy
. W rzeczywistości wydaje się, że nastąpił wzrost wydajności.
Wyjaśnienie wyników
Dlaczego więc miałby std::copy
zwiększyć wydajność?
Po pierwsze, nie spodziewałbym się, że będzie wolniej w przypadku jakiejkolwiek implementacji, o ile włączona jest optymalizacja inliningu. Wszystkie kompilatory działają agresywnie; jest to prawdopodobnie najważniejsza optymalizacja, ponieważ umożliwia wiele innych optymalizacji. std::copy
może (i podejrzewam, że wszystkie implementacje w świecie rzeczywistym tak robią) wykryć, że argumenty można w prosty sposób skopiować, a pamięć jest ułożona sekwencyjnie. Oznacza to, że w najgorszym przypadku, kiedy memcpy
jest to legalne, nie std::copy
powinno działać gorzej. Prosta implementacja std::copy
tego odracza, memcpy
powinna spełniać kryteria twojego kompilatora „zawsze wstaw to przy optymalizacji pod kątem szybkości lub rozmiaru”.
Jednak std::copy
zachowuje również więcej swoich informacji. Kiedy dzwonisz std::copy
, funkcja zachowuje typy nienaruszone. memcpy
działa na void *
, który odrzuca prawie wszystkie przydatne informacje. Na przykład, jeśli przekażę tablicę std::uint64_t
, kompilator lub implementator biblioteki może być w stanie skorzystać z 64-bitowego wyrównania z std::copy
, ale może to być trudniejsze do zrobienia z memcpy
. Wiele implementacji algorytmów, takich jak ten, działa najpierw na części niewyrównanej na początku zakresu, następnie na części wyrównanej, a następnie na części bez wyrównania na końcu. Jeśli wszystko jest na pewno wyrównane, kod staje się prostszy i szybszy, a predyktor rozgałęzień w procesorze będzie łatwiejszy do poprawienia.
Przedwczesna optymalizacja?
std::copy
jest w interesującej pozycji. Spodziewam się, że nigdy nie będzie wolniejszy, memcpy
a czasem szybszy w przypadku każdego nowoczesnego kompilatora optymalizującego. Co więcej, wszystko, co możesz memcpy
, możesz std::copy
. memcpy
nie pozwala na nakładanie się zderzaków, podczas gdy std::copy
podpory zachodzą na siebie w jednym kierunku (z std::copy_backward
zachodzeniem na drugi kierunek). memcpy
działa tylko na wskaźnikach, std::copy
działa na każdej iteratorów ( std::map
, std::vector
, std::deque
, lub mój własny niestandardowy typ). Innymi słowy, powinieneś używać go tylko std::copy
wtedy, gdy chcesz skopiować fragmenty danych.
char
może być podpisane lub niepodpisane, w zależności od implementacji. Jeśli liczba bajtów może być> = 128, użyjunsigned char
dla tablic bajtów. ((int *)
Obsada też byłaby bezpieczniejsza(unsigned int *)
.)