Czy lepiej jest używać, memcpy
jak pokazano poniżej, czy lepiej jest używać std::copy()
pod względem wydajności? Czemu?
char *bits = NULL;
...
bits = new (std::nothrow) char[((int *) copyMe->bits)[0]];
if (bits == NULL)
{
cout << "ERROR Not enough memory.\n";
exit(1);
}
memcpy (bits, copyMe->bits, ((int *) copyMe->bits)[0]);
c++
performance
optimization
user576670
źródło
źródło
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 *)
.)std::vector<char>
? Albo skoro mówiszbits
,std::bitset
?(int*) copyMe->bits[0]
robi?int
dyktuje jego rozmiar, ale wydaje się, że jest to przepis na katastrofę zdefiniowaną w implementacji, jak wiele innych rzeczy tutaj.(int *)
rzutowanie jest po prostu czystym niezdefiniowanym zachowaniem, a nie zdefiniowanym w implementacji. Próba wykonania punningu za pomocą rzutowania narusza ścisłe zasady aliasingu i dlatego jest całkowicie nieokreślona przez Standard. (Również w C ++, chociaż nie w C, nie możesz pisać kalambur za pomocąunion
albo.) Prawie jedynym wyjątkiem jest sytuacja, gdy konwertujesz na wariantchar*
, ale dodatek nie jest symetryczny.Odpowiedzi:
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ą istd::copy
wersją. Mój kod korzysta z przechwytywania danych w jak największej liczbie fragmentów (wiele innych implementacji działa zchar
/char *
, podczas gdy ja operuję zT
/T *
(gdzieT
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
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
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łastd::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
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, kiedymemcpy
jest to legalne, niestd::copy
powinno działać gorzej. Prosta implementacjastd::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 dzwoniszstd::copy
, funkcja zachowuje typy nienaruszone.memcpy
działa navoid *
, 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 zstd::copy
, ale może to być trudniejsze do zrobienia zmemcpy
. 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żeszmemcpy
, możeszstd::copy
.memcpy
nie pozwala na nakładanie się zderzaków, podczas gdystd::copy
podpory zachodzą na siebie w jednym kierunku (zstd::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 tylkostd::copy
wtedy, gdy chcesz skopiować fragmenty danych.źródło
std::copy
to, że jest to 2,99% lub 0,72% lub -0,11% szybsze niżmemcpy
te czasy dla całego programu. Jednak generalnie uważam, że testy porównawcze w prawdziwym kodzie są bardziej przydatne niż testy porównawcze w fałszywym kodzie. Mój cały program miał tę zmianę w szybkości wykonywania. Rzeczywiste efekty tylko dwóch schematów kopiowania będą miały większe różnice niż pokazano tutaj, gdy są rozpatrywane osobno, ale to pokazuje, że mogą mieć mierzalne różnice w rzeczywistym kodzie.memcpy
istd::copy
ma różne implementacje, więc w niektórych przypadkach kompilator optymalizuje otaczający kod i rzeczywisty kod kopiowania pamięci jako jeden integralny fragment kodu. Innymi słowy, czasami jeden jest lepszy od drugiego, a nawet innymi słowy, decyzja, który z nich zastosuje, jest przedwczesną, a nawet głupią optymalizacją, bo w każdej sytuacji trzeba zrobić nowe badania i co więcej, programy są zwykle rozwijane, więc po niektóre drobne zmiany mogą zostać utracone przewaga funkcji nad innymi.std::copy
jest to trywialna funkcja wbudowana, która wywołuje tylkomemcpy
wtedy, gdy jest to legalne. Podstawowe podszycie wyeliminowałoby wszelkie negatywne różnice w wydajności. Zaktualizuję post z wyjaśnieniem, dlaczego std :: copy może być szybsze.Wszystkie kompilatory, które znam, zastępują prosty
std::copy
element a,memcpy
gdy jest to odpowiednie, a nawet lepiej, wektoryzują kopię, aby była jeszcze szybsza niżmemcpy
.W każdym razie: profiluj i przekonaj się sam. Różne kompilatory będą robić różne rzeczy i jest całkiem możliwe, że nie zrobią dokładnie tego, o co prosisz.
Zobacz tę prezentację na temat optymalizacji kompilatora (pdf).
Oto, co robi GCC dla prostego
std::copy
typu POD.Oto demontaż (tylko z
-O
optymalizacją), pokazujący wywołaniememmove
:Jeśli zmienisz podpis funkcji na
następnie
memmove
staje się amemcpy
dla niewielkiej poprawy wydajności. Zauważ, żememcpy
sam będzie silnie wektoryzowany.źródło
memmove
nie powinien być szybszy - raczej powinien być wolniejszy, ponieważ musi uwzględniać możliwość nakładania się dwóch zakresów danych. Myślę, żestd::copy
zezwala na nakładanie się danych, więc musi zadzwonićmemmove
.memcpy
. To prowadzi mnie do przekonania, że GCC sprawdza, czy pamięć się pokrywa.std::copy
pozwala na nakładanie się w jednym kierunku, ale nie w drugim. Początek wyjścia nie może leżeć w zakresie wejściowym, ale początek wejścia może leżeć w zakresie wyjściowym. Jest to trochę dziwne, ponieważ kolejność przypisań jest zdefiniowana, a wywołanie może być UB, nawet jeśli efekt tych przypisań w tej kolejności jest zdefiniowany. Ale przypuszczam, że ograniczenie umożliwia optymalizację wektoryzacji.Zawsze należy używać
std::copy
, ponieważmemcpy
ogranicza się tylko w stylu C struktur POD, a kompilator może zastąpić wywołaństd::copy
zememcpy
jeśli cele są w rzeczywistości POD.Ponadto
std::copy
może być używany z wieloma typami iteratorów, a nie tylko ze wskaźnikami.std::copy
jest bardziej elastyczny, aby nie tracić wydajności i jest wyraźnym zwycięzcą.źródło
std::copy(container.begin(), container.end(), destination);
skopiuje zawartośćcontainer
(wszystko pomiędzybegin
iend
) do bufora wskazanego przezdestination
.std::copy
nie wymaga oszustw takich jak&*container.begin()
lub&container.back() + 1
.Teoretycznie
memcpy
może mieć niewielką , niezauważalną , nieskończenie małą przewagę wydajności, tylko dlatego, że nie ma takich samych wymagań jakstd::copy
. Ze strony podręcznika manmemcpy
:Innymi słowy,
memcpy
może zignorować możliwość nakładania się danych. (Przekazywanie nakładających się tablic domemcpy
jest niezdefiniowanym zachowaniem).memcpy
Nie trzeba więc jawnie sprawdzać tego warunku, natomiaststd::copy
można go używać, o ileOutputIterator
parametr nie znajduje się w zakresie źródłowym. Pamiętaj, że to nie to samo, co stwierdzenie, że zakres źródłowy i docelowy nie mogą się pokrywać.Więc ponieważ
std::copy
ma nieco inne wymagania, teoretycznie powinien być nieco (z ekstremalnym naciskiem na nieco ) wolniejszy, ponieważ prawdopodobnie sprawdzi nakładające się tablice C lub deleguje kopiowanie tablic C domemmove
, który musi wykonać czek. Ale w praktyce Ty (i większość osób zajmujących się profilowaniem) prawdopodobnie nie zauważysz żadnej różnicy.Oczywiście, jeśli nie pracujesz z POD , i tak nie możesz z niego korzystać
memcpy
.źródło
std::copy<char>
. Alestd::copy<int>
można założyć, że jego dane wejściowe są wyrównane wewnętrznie. To będzie o wiele większa różnica, ponieważ wpływa na każdy element. Nakładanie się to jednorazowa kontrola.memcpy
, które widziałem, sprawdza wyrównanie i próbuje kopiować słowa, a nie bajt po bajcie.memcpy
interfejs traci informacje o wyrównaniu. W związku z tymmemcpy
musi wykonywać sprawdzanie wyrównania w czasie wykonywania, aby obsłużyć niewyrównane początki i końce. Te czeki mogą być tanie, ale nie są darmowe. Natomiaststd::copy
można uniknąć tych kontroli i wektoryzować. Ponadto kompilator może udowodnić, że tablice źródłowe i docelowe nie nakładają się i ponownie wektoryzują bez konieczności wybierania przez użytkownika międzymemcpy
imemmove
.Moja zasada jest prosta. Jeśli używasz C ++, preferuj biblioteki C ++, a nie C :)
źródło
std::end(c_arr)
zamiast tegoc_arr + i_hope_this_is_the_right_number_of elements
jest bezpieczniejsze? a co ważniejsze, jaśniejsze. I to byłby punkt, na który kładę nacisk w tym konkretnym przypadku:std::copy()
jest bardziej idiomatyczny, łatwiejszy w utrzymaniu, jeśli typy iteratorów zmieniają się później, prowadzi do jaśniejszej składni itp.std::copy
jest bezpieczniejsze, ponieważ poprawnie kopiuje przekazane dane, jeśli nie są one typami POD.memcpy
szczęśliwie skopiujestd::string
obiekt do nowej reprezentacji bajt po bajcie.Tylko niewielki dodatek: różnica prędkości między
memcpy()
istd::copy()
może się znacznie różnić w zależności od tego, czy optymalizacje są włączone, czy wyłączone. Z g ++ 6.2.0 i bez optymalizacjimemcpy()
wyraźnie wygrywa:Gdy optymalizacje są włączone (
-O3
), wszystko znowu wygląda prawie tak samo:Im większa tablica, tym mniej zauważalny jest efekt, ale nawet przy
N=1000
memcpy()
jest około dwa razy szybciej, gdy optymalizacje nie są włączone.Kod źródłowy (wymaga testu porównawczego Google):
źródło
Jeśli naprawdę potrzebujesz maksymalnej wydajności kopiowania (której możesz nie mieć), nie używaj żadnego z nich .
Istnieje wiele , że można zrobić, aby zoptymalizować kopiowania pamięci - nawet więcej, jeśli jesteś gotów do korzystania z wielu wątków / rdzeni dla niego. Zobacz na przykład:
Czego brakuje / nie jest optymalne w tej implementacji memcpy?
zarówno pytanie, jak i niektóre odpowiedzi mają sugerowane implementacje lub linki do implementacji.
źródło
Profilowanie pokazuje, że stwierdzenie:
std::copy()
jest zawsze tak szybkie, jakmemcpy()
lub szybciej jest fałszywe.Mój system:
Kod (język: c ++):
Red Alert zwrócił uwagę, że kod używa memcpy z tablicy do tablicy i std :: copy z tablicy do wektora. To może być powodem do szybszego memcpy.
Skoro jest
v.reserve (sizeof (arr1));
nie będzie różnicy w kopiowaniu do wektora lub tablicy.
Kod jest ustawiony tak, aby używał tablicy w obu przypadkach. memcpy jeszcze szybciej:
źródło
std::copy
z wektora do tablicymemcpy
trwało prawie dwa razy dłużej? Te dane są wysoce podejrzane. Skompilowałem twój kod przy użyciu gcc z -O3, a wygenerowany zestaw jest taki sam dla obu pętli. Zatem każda różnica w czasie, którą obserwujesz na swoim komputerze, jest tylko przypadkowa.