Чому memmove швидший, ніж memcpy?


89

Я досліджую гарячі точки продуктивності в додатку, який проводить 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

Я провів кілька варіацій тестів на основі різних відповідей.

  1. При запуску memcpy двічі, тоді другий запуск відбувається швидше, ніж перший.
  2. При "торканні" цільового буфера memcpy ( memset(b2, 0, BUFFERSIZE...)) тоді перший запуск memcpy також відбувається швидше.
  3. 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.

Дякуємо за всі чудові відповіді та коментарі!


1
Ви дивились на код memmove, ви також дивились на код memcpy?
Олівер Чарлсворт,

8
Я очікував, що копіювання пам’яті відбувається надзвичайно швидко - лише тоді, коли пам’ять знаходиться в кеші L1. Коли дані не поміщаються в кеші, ефективність копіювання зменшується.
Максим Єгорушкін

1
До речі, ви скопіювали лише одну гілку memmove. Ця гілка не може обробляти переміщення, коли джерело перекриває пункт призначення і пункт призначення знаходиться за нижчими адресами.
Максим Єгорушкін

2
Я не встиг отримати доступ до машини Linux, тому поки що не можу перевірити цю теорію. Але іншим можливим поясненням є надмірність ; ваш memcpyцикл є першим доступом до вмісту b2, тому ОС повинна виділяти для нього фізичну пам’ять у міру його дії.
Олівер Чарлсворт,

2
PS: Якщо це вузьке місце, я б переглянути підхід. Як щодо введення значень у список або деревоподібну структуру (наприклад, двійкове дерево), а потім зчитування їх у масив наприкінці. Вузли в такому підході будуть чудовим кандидатом для розподілу пулів. Вони додаються лише до кінця, коли їх масово звільняють. Це особливо вірно, якщо ви знаєте, скільки вам знадобиться на самому початку. Бібліотеки підсилення мають розподілювач пулів.
Persixty

Відповіді:


57

Ваші memmoveдзвінки переміщують пам’ять на 2–128 байт, тоді як memcpyджерело та пункт призначення повністю відрізняються. Це якимось чином враховує різницю в продуктивності: якщо ви скопіюєте в одне і те ж місце, ви побачите memcpy, можливо, шматок швидше, наприклад, на ideone.com :

memmove (002) 0.0610362
memmove (004) 0.0554264
memmove (008) 0.0575859
memmove (016) 0.057326
memmove (032) 0.0583542
memmove (064) 0.0561934
memmove (128) 0.0549391
memcpy 0.0537919

Навряд чи щось у ньому - жодних доказів того, що повернення на вже зіпсовану сторінку пам'яті справляє сильний вплив, і ми, звичайно, не бачимо зменшення часу вдвічі ... але це показує, що немає нічого поганого в тому, що робити memcpyнепотрібно повільніше, якщо порівнювати яблука -для яблук.


Я би очікував, що кеш-пам'яті процесора не спричиняють різниці, оскільки мої буфери набагато більші, ніж кеші.
cruppstahl

2
Але для кожного потрібна однакова загальна кількість доступу до основної пам'яті, так? (Тобто 100 МБ читання та 100 МБ запису). Шаблон кешу не обходить це. Тож єдиний спосіб, який може бути повільнішим за інший, полягає в тому, що деякі матеріали потрібно читати / записувати з / в пам’ять більше одного разу.
Олівер Чарлсворт,

2
@Tony D - Моїм висновком було запитати людей, які розумніші за мене;)
cruppstahl

1
Крім того, що трапиться, якщо ви скопіюєте в те саме місце, але зробите memcpyспочатку знову?
Олівер Чарльзворт,

1
@OliverCharlesworth: перший тестовий запуск завжди приймає значний удар, але виконуючи два тести memcpy: memcpy 0,0688002 0,0583162 | memmove 0,0577443 0,05862 0,0601029 ... див. ideone.com/8EEAcA
Тоні Делрой

25

Коли ви використовуєте memcpy, записи повинні йти в кеш. Коли ви використовуєте memmoveде, коли ви копіюєте невеликий крок вперед, пам'ять, над якою ви копіюєте, вже буде в кеші (оскільки вона була прочитана 2, 4, 16 або 128 байт "назад"). Спробуйте зробити там, memmoveде пункт призначення - кілька мегабайт (> 4 * розмір кеш-пам’яті), і я підозрюю (але це не може турбуватися для тестування), що ви отримаєте подібні результати.

Я гарантую, що ALL стосується обслуговування кешу, коли ви робите великі операції з пам'яттю.


+1 Я думаю, з причин, про які ви згадали, зворотний цикл memmove зручніший кеш, ніж memcpy. Однак я виявив, що при запуску тесту memcpy двічі другий запуск є таким же швидким, як memmove. Чому? Буфери настільки великі, що другий запуск memcpy повинен бути таким же неефективним (кеш-пам'ять), як і перший запуск. Тож, схоже, тут є додаткові фактори, які спричиняють покарання за продуктивність.
cruppstahl

3
За належних обставин секунда memcpyбуде помітно швидшою просто тому, що TLB попередньо заповнений. Крім того, на секунду memcpyне доведеться спорожняти кеш речей, від яких вам може знадобитися "позбутися" (брудні кеш-лінії "погані" для продуктивності у багатьох відношеннях. Однак, щоб сказати точно, вам потрібно буде запустити щось на кшталт "perf" та відібрати такі речі, як пропуски кешу, пропуски TLB тощо.
Mats Petersson

15

Історично пам'ять і мемкопія - це однакові функції. Вони працювали однаково і мали однакову реалізацію. Тоді було зрозуміло, що мемкопію не потрібно визначати (і часто не визначали) для обробки перекриваються областей якимось конкретним чином.

Кінцевим результатом є те, що memmove було визначено для обробки перекриваються областей певним чином, навіть якщо це впливає на продуктивність. Мемкопія повинна використовувати найкращий алгоритм, доступний для регіонів, що не перекриваються. Реалізації, як правило, майже однакові.

Проблема, з якою ви зіткнулися, полягає в тому, що існує так багато різновидів обладнання x86, що неможливо сказати, який спосіб переміщення пам’яті буде найшвидшим. І навіть якщо ви думаєте, що у вас є результат за однієї обставини, щось настільки просте, як наявність іншого «кроку» в розташуванні пам’яті, може спричинити значно різну продуктивність кешу.

Ви можете або порівняти те, що ви насправді робите, або проігнорувати проблему і покластися на тести, зроблені для бібліотеки C.

Редагувати: О, і останнє; перенесення великої кількості вмісту пам'яті ДУЖЕ повільно. Я гадаю, що ваш додаток буде працювати швидше, щось на зразок простої реалізації B-Tree для обробки цілих чисел. (О, ти добре, добре)

Edit2: Підсумовуючи моє розширення в коментарях: Мікробенчмарк - це проблема тут, вона не вимірює того, що ви думаєте. Завдання, які даються memcpy та memmove, суттєво відрізняються між собою. Якщо завдання, віддане memcpy, повторюється кілька разів за допомогою memmove або memcpy, кінцеві результати не залежатимуть від того, яку функцію переміщення пам'яті ви використовуєте, ДОКЛИ регіони не перекриваються.


Але саме про це йдеться - я порівняю те, що я насправді роблю. Це питання стосується інтерпретації результатів еталону, які суперечать тому, що ви стверджуєте, - що memcpy швидший для неперекриваючих регіонів.
cruppstahl

Моя програма - b-дерево! Кожного разу, коли цілі числа вставляються в листовий вузол, memmove викликається для звільнення місця. Я працюю над механізмом баз даних.
cruppstahl

1
Ви використовуєте мікротест, і навіть не потрібно, щоб мемкопія та меммове переміщували однакові дані. Точне розташування в пам’яті, в якому перебувають дані, з якими ви обробляєтесь, різниться до кешування та кількості обернених поїздок до пам'яті, яку повинен зробити процесор.
user3710044

Хоча ця відповідь правильна, насправді вона не пояснює, чому в цьому випадку вона повільніша, вона, по суті, говорить "це повільніше, тому що в деяких випадках вона може бути повільнішою".
Олівер Чарлсворт,

Я кажу, що за тих самих обставин, включаючи однаковий макет пам'яті для копіювання / переміщення тестів, БУДУТЬ однаковими, оскільки реалізації однакові. Проблема в мікровимірюванні.
user3710044

2

"memcpy ефективніший, ніж memmove." У вашому випадку ви, швидше за все, не робите однаково те саме, поки запускаєте дві функції.

Загалом, USE memmove лише за потреби. ВИКОРИСТОВУЙТЕ, коли існує цілком обґрунтована ймовірність того, що регіони джерела та пункту призначення перекриваються.

Довідково: https://www.youtube.com/watch?v=Yr1YnOVG-4g Доктор Джеррі Кейн, (Лекція про вступні системи в Стенфорді - 7) Час: 36:00

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.