Я изучаю горячие точки производительности в приложении, которое тратит 50% его время в memmove (3). Приложение вставляет миллионы 4-байтных целых чисел в отсортированные массивы и использует memmove для смещения данных "вправо" в чтобы освободить место для вставленного значения.
Мое ожидание заключалось в том, что копирование памяти происходит очень быстро, и я был удивлен что столько времени тратится на память. Но тогда у меня возникла идея, что memmove является медленным, поскольку он перемещает перекрывающиеся области, которые должны быть реализованы в жесткой петле, вместо копирования больших страниц памяти. Я написал небольшую microbenchmark, чтобы узнать, есть ли разница в производительности между memcpy и memmove, ожидая, что memcpy выиграет руки.
Я запустил свой бенчмарк на двух машинах (ядро i5, ядро i7) и увидел, что memmove на самом деле быстрее, чем memcpy, на старшем ядре i7 даже почти в два раза быстрее! Теперь я ищу объяснения.
Вот мой бенчмарк. Он копирует 100 мб с memcpy, а затем перемещает около 100 мб с memmove; источник и место назначения перекрываются. Различные "расстояния" для источника и назначения. Каждый тест выполняется 10 раз, средний время печатается.
https://gist.github.com/cruppstahl/78a57cdf937bca3d062c
Вот результаты на Core i5 (Linux 3.5.0-54-generiС# 81 ~ exact1-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 и С++. Этот вопрос действительно просто для академических целей.
ОБНОВЛЕНИЕ 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.
Спасибо за отличные ответы и комментарии!