Mam zamiar sprzeciwić się ogólnej mądrości, która std::copyspowoduje 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 memcpywersją i std::copywersją. 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 Tjest 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::copyktó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::copyznowu 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::copyzwię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::copymoż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 memcpyjest to legalne, nie std::copypowinno działać gorzej. Prosta implementacja std::copytego odracza, memcpypowinna spełniać kryteria twojego kompilatora „zawsze wstaw to przy optymalizacji pod kątem szybkości lub rozmiaru”.
Jednak std::copyzachowuje również więcej swoich informacji. Kiedy dzwonisz std::copy, funkcja zachowuje typy nienaruszone. memcpydział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::copyjest w interesującej pozycji. Spodziewam się, że nigdy nie będzie wolniejszy, memcpya czasem szybszy w przypadku każdego nowoczesnego kompilatora optymalizującego. Co więcej, wszystko, co możesz memcpy, możesz std::copy. memcpynie pozwala na nakładanie się zderzaków, podczas gdy std::copypodpory zachodzą na siebie w jednym kierunku (z std::copy_backwardzachodzeniem na drugi kierunek). memcpydziała tylko na wskaźnikach, std::copydział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::copywtedy, gdy chcesz skopiować fragmenty danych.
charmoże być podpisane lub niepodpisane, w zależności od implementacji. Jeśli liczba bajtów może być> = 128, użyjunsigned chardla tablic bajtów. ((int *)Obsada też byłaby bezpieczniejsza(unsigned int *).)