Badam punkty aktywne wydajności w aplikacji, która spędza 50% czasu w memmove (3). Aplikacja wstawia miliony 4-bajtowych liczb całkowitych do posortowanych tablic i używa memmove do przesunięcia danych „w prawo” w celu zwolnienia miejsca na wstawioną wartość.
Spodziewałem się, że kopiowanie pamięci będzie niezwykle szybkie i byłem zaskoczony, że tak dużo czasu spędzam w memmove. Ale wtedy wpadłem na pomysł, że memmove jest powolne, ponieważ porusza nakładające się regiony, które muszą być realizowane w ciasnej pętli, zamiast kopiować duże strony pamięci. Napisałem mały mikrobenchmark, aby dowiedzieć się, czy istnieje różnica w wydajności między memcpy i memmove, spodziewając się, że memcpy wygra bez wątpienia.
Przeprowadziłem benchmark na dwóch maszynach (core i5, core i7) i zobaczyłem, że memmove jest w rzeczywistości szybszy niż memcpy, na starszym rdzeniu i7 nawet prawie dwa razy szybciej! Teraz szukam wyjaśnień.
Oto mój punkt odniesienia. Kopiuje 100 MB za pomocą memcpy, a następnie przesuwa się około 100 MB za pomocą memmove; źródło i miejsce docelowe nakładają się. Próbuje się różnych „odległości” dla źródła i celu. Każdy test jest wykonywany 10 razy, średni czas jest drukowany.
https://gist.github.com/cruppstahl/78a57cdf937bca3d062c
Oto wyniki dla Core i5 (Linux 3.5.0-54-generic # 81 ~ exact1-Ubuntu SMP x86_64 GNU / Linux, gcc to 4.6.3 (Ubuntu / Linaro 4.6.3-1ubuntu5). Liczba w nawiasach to odległość (wielkość przerwy) między źródłem a celem:
memcpy 0.0140074
memmove (002) 0.0106168
memmove (004) 0.01065
memmove (008) 0.0107917
memmove (016) 0.0107319
memmove (032) 0.0106724
memmove (064) 0.0106821
memmove (128) 0.0110633
Memmove jest implementowany jako zoptymalizowany kod asemblera SSE, kopiujący od tyłu do przodu. Używa wstępnego pobierania sprzętowego do załadowania danych do pamięci podręcznej i kopiuje 128 bajtów do rejestrów XMM, a następnie przechowuje je w miejscu docelowym.
( memcpy-ssse3-back.S , wiersze 1650 i następne)
L(gobble_ll_loop):
prefetchnta -0x1c0(%rsi)
prefetchnta -0x280(%rsi)
prefetchnta -0x1c0(%rdi)
prefetchnta -0x280(%rdi)
sub $0x80, %rdx
movdqu -0x10(%rsi), %xmm1
movdqu -0x20(%rsi), %xmm2
movdqu -0x30(%rsi), %xmm3
movdqu -0x40(%rsi), %xmm4
movdqu -0x50(%rsi), %xmm5
movdqu -0x60(%rsi), %xmm6
movdqu -0x70(%rsi), %xmm7
movdqu -0x80(%rsi), %xmm8
movdqa %xmm1, -0x10(%rdi)
movdqa %xmm2, -0x20(%rdi)
movdqa %xmm3, -0x30(%rdi)
movdqa %xmm4, -0x40(%rdi)
movdqa %xmm5, -0x50(%rdi)
movdqa %xmm6, -0x60(%rdi)
movdqa %xmm7, -0x70(%rdi)
movdqa %xmm8, -0x80(%rdi)
lea -0x80(%rsi), %rsi
lea -0x80(%rdi), %rdi
jae L(gobble_ll_loop)
Dlaczego memmove jest szybsze niż memcpy? Spodziewałbym się, że memcpy skopiuje strony pamięci, co powinno być znacznie szybsze niż zapętlanie. W najgorszym przypadku spodziewałbym się, że memcpy będzie tak samo szybkie jak memmove.
PS: Wiem, że w moim kodzie nie mogę zamienić memmove na memcpy. Wiem, że przykładowy kod łączy C i C ++. To pytanie jest naprawdę tylko do celów akademickich.
AKTUALIZACJA 1
Przeprowadziłem kilka odmian testów w oparciu o różne odpowiedzi.
- Przy dwukrotnym uruchomieniu memcpy drugi bieg jest szybszy niż pierwszy.
- Kiedy "dotykasz" bufora docelowego memcpy (
memset(b2, 0, BUFFERSIZE...)), to pierwsze uruchomienie memcpy jest również szybsze. - memcpy jest wciąż trochę wolniejszy niż memmove.
Oto wyniki:
memcpy 0.0118526
memcpy 0.0119105
memmove (002) 0.0108151
memmove (004) 0.0107122
memmove (008) 0.0107262
memmove (016) 0.0108555
memmove (032) 0.0107171
memmove (064) 0.0106437
memmove (128) 0.0106648
Mój wniosek: na podstawie komentarza @Oliver Charlesworth, system operacyjny musi zatwierdzić pamięć fizyczną, gdy tylko bufor docelowy memcpy zostanie uzyskany po raz pierwszy (jeśli ktoś wie, jak to „udowodnić”, dodaj odpowiedź! ). Ponadto, jak powiedział @Mats Petersson, memmove jest bardziej przyjazny dla pamięci podręcznej niż memcpy.
Dzięki za wszystkie świetne odpowiedzi i komentarze!