Я исследую «горячие точки» производительности в приложении, которое 50% времени проводит в memmove (3). Приложение вставляет миллионы 4-байтовых целых чисел в отсортированные массивы и использует memmove для смещения данных «вправо», чтобы освободить место для вставленного значения.
Я ожидал, что копирование памяти происходит очень быстро, и был удивлен, что в memmove тратится так много времени. Но потом у меня возникла идея, что memmove работает медленно, потому что он перемещает перекрывающиеся области, которые должны быть реализованы в тесном цикле, вместо того, чтобы копировать большие страницы памяти. Я написал небольшой микробенчмарк, чтобы выяснить, есть ли разница в производительности между 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 ~ точный1-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 и далее)
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.
Спасибо за отличные ответы и комментарии!