Я досліджую гарячі точки продуктивності в додатку, який проводить 50% свого часу в memmove (3). Додаток вставляє мільйони 4-байтових цілих чисел у відсортовані масиви та використовує memmove для зсуву даних "вправо", щоб звільнити місце для вставленого значення.
Я сподівався, що копіювання пам'яті відбувається надзвичайно швидко, і я був здивований тим, що стільки часу проводить у memmove. Але тоді у мене виникла ідея, що memmove повільний, оскільки він переміщує перекриваються регіони, які повинні бути реалізовані в щільному циклі, замість копіювання великих сторінок пам'яті. Я написав невеликий мікробенчмарк, щоб з'ясувати, чи є різниця в продуктивності між memcpy та memmove, очікуючи, що memcpy виграє руки.
Я провів свій орієнтир на двох машинах (core i5, core i7) і побачив, що memmove насправді швидший, ніж memcpy, на старшому ядрі i7 навіть майже вдвічі швидше! Зараз я шукаю пояснень.
Ось мій орієнтир. Він копіює 100 Мб за допомогою memcpy, а потім переміщує близько 100 Мб за допомогою memmove; джерело та адреса перекриваються. Випробовуються різні "відстані" до джерела та пункту призначення. Кожен тест проводиться 10 разів, друкується середній час.
https://gist.github.com/cruppstahl/78a57cdf937bca3d062c
Ось результати щодо Core i5 (Linux 3.5.0-54-generic # 81 ~preci1-Ubuntu SMP x86_64 GNU / Linux, gcc становить 4.6.3 (Ubuntu / Linaro 4.6.3-1ubuntu5). Кількість у дужках відстань (розмір зазору) між джерелом та пунктом призначення:
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 реалізований як оптимізований для SSE код асемблера, копіюючи ззаду наперед. Він використовує апаратне попереднє завантаження для завантаження даних у кеш-пам’ять, копіює 128 байт у регістри XMM, а потім зберігає їх у місці призначення.
( memcpy-ssse3-back.S , рядки 1650 ff)
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)
Чому memmove швидше, ніж memcpy? Я би очікував, що memcpy копіює сторінки пам'яті, що має бути набагато швидше, ніж циклічне. У гіршому випадку я би очікував, що memcpy буде таким швидким, як memmove.
PS: Я знаю, що не можу замінити memmove на memcpy у своєму коді. Я знаю, що зразок коду поєднує C і C ++. Це питання насправді стосується лише академічних цілей.
ОНОВЛЕННЯ 1
Я провів кілька варіацій тестів на основі різних відповідей.
- При запуску memcpy двічі, тоді другий запуск відбувається швидше, ніж перший.
- При "торканні" цільового буфера memcpy (
memset(b2, 0, BUFFERSIZE...)
) тоді перший запуск memcpy також відбувається швидше. - memcpy все ще трохи повільніший за memmove.
Ось результати:
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
Мій висновок: на основі коментаря @Oliver Charlesworth, операційна система повинна зафіксувати фізичну пам'ять, як тільки буфер призначення memcpy доступний уперше (якщо хтось знає, як "довести" це, будь ласка, додайте відповідь! ). Крім того, як сказав @Mats Petersson, memmove є кеш зручнішим за memcpy.
Дякуємо за всі чудові відповіді та коментарі!