Почему memmove быстрее, чем memcpy?


Я исследую горячие точки производительности в приложении, которое тратит 50% время в memmove языка(3). Приложение вставляет миллионы 4-байтовых целых чисел в отсортированных массивах и использует memmove для сдвига данных "вправо" в чтобы освободить место для вставляемого значения.

Я ожидал, что копирование памяти происходит очень быстро, и я был удивлен что столько времени тратится на memmove языка. Но потом мне пришла в голову мысль, что memmove медленно, потому что он движется перекрывая регионов, которые должны быть реализованы в плотном цикле, вместо копирования больших страниц памяти. Я написал небольшой microbenchmark, чтобы узнать, есть ли разница в производительности между memcpy и memmove, ожидая, что memcpy выиграет руки вниз.

Я запустил свой тест на двух машинах (core i5, core i7) и увидел, что memmove на самом деле быстрее, чем memcpy, на старом core i7 даже почти в два раза быстрее! Теперь я ищу объяснения.

вот мой тест. Оно копирует 100 МБ с помощью memcpy, а затем перемещает около 100 МБ с помощью memmove; источник и назначение перекрываются. Различные " расстояния" для источника и назначения пробовали. Каждый тест выполняется 10 раз, в среднем время печати.

https://gist.github.com/cruppstahl/78a57cdf937bca3d062c

вот результаты на Core i5 (Linux 3.5.0-54-generic #81~precise1-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-назад.S, строки 1650 ff)

L(gobble_ll_loop):
    prefetchnta -0x1c0(%rsi)
    prefetchnta -0x280(%rsi)
    prefetchnta -0x1c0(%rdi)
    prefetchnta -0x280(%rdi)
    sub x80, %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.

Спасибо огромное за ваши ответы и комментарии!

4 80

4 ответа:

код 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 излишне медленнее по сравнению с яблоками для яблок.

при использовании memcpy, записи должны идти в кэш. Когда вы используете memmove где, когда вы копируете небольшой шаг вперед, память, которую вы копируете, уже будет в кэше (потому что она была прочитана 2, 4, 16 или 128 байт "назад"). Попробуйте сделать memmove здесь несколько мегабайт (> 4 * размер кэша), и я подозреваю (но не могу быть обеспокоен, чтобы проверить), что вы получите аналогичные результаты.

Я гарантирую, что все дело в обслуживании кэша, когда вы делать большие операции с памятью.

исторически memmove и memcopy являются одной и той же функцией. Они работали одинаково и имели одинаковую реализацию. Затем было понято, что memcopy не должна быть (и часто не была) определена для обработки перекрывающихся областей каким-либо конкретным образом.

конечным результатом является то, что memmove был определен для обработки перекрывающихся областей определенным образом, даже если это влияет на производительность. Предполагается, что Memcopy использует лучший алгоритм, доступный для неперекрывающихся областей. Этот реализация, как правило, практически идентичны.

проблема, с которой вы столкнулись, заключается в том, что существует так много вариантов оборудования x86, что невозможно сказать, какой метод сдвига памяти будет самым быстрым. И даже если вы думаете, что у вас есть результат в одном обстоятельстве, что-то такое же простое, как наличие другого "шага" в макете памяти, может вызвать совершенно другую производительность кэша.

вы можете либо проверить, что вы на самом деле делаете, либо игнорировать проблема и полагаться на тесты, сделанные для библиотеки C.

Edit: О, И последнее; перемещение большого количества содержимого памяти вокруг очень медленно. Я бы предположил, что ваше приложение будет работать быстрее с чем-то вроде простой реализации B-дерева для обработки ваших целых чисел. (О да, хорошо)

Edit2: чтобы подвести итог моего расширения в комментариях: Проблема заключается в том, что microbenchmark не измеряет то, что вы думаете. Задачи, поставленные перед memcpy и memmove отличаются значительно друг от друга. Если задача, заданная memcpy, повторяется несколько раз с memmove или memcpy, конечные результаты не будут зависеть от того, какую функцию сдвига памяти вы используете, Если области не перекрываются.

"memcpy является более эффективным, чем memmove."В вашем случае, вы, скорее всего, не делаете то же самое, когда вы запускаете две функции.

В общем, используйте memmove только в случае необходимости. Используйте его, когда есть очень разумная вероятность того, что исходная и конечная области перегружены.

Ссылка:https://www.youtube.com/watch?v=Yr1YnOVG-4g доктор Джерри Кейн, (Stanford Intro Systems Lecture-7) Время: 36:00