Estoy investigando puntos críticos de rendimiento en una aplicación que pasa el 50% de su tiempo en memmove (3). La aplicación inserta millones de enteros de 4 bytes en matrices ordenadas y utiliza memmove para desplazar los datos "hacia la derecha" para dejar espacio para el valor insertado.
Mi expectativa era que copiar la memoria sea extremadamente rápido, y me sorprendió que se invierta tanto tiempo en memmove. Pero luego tuve la idea de que memmove es lento porque mueve regiones superpuestas, que deben implementarse en un bucle cerrado, en lugar de copiar grandes páginas de memoria. Escribí un pequeño microbenchmark para averiguar si había una diferencia de rendimiento entre memcpy y memmove, esperando que memcpy ganara sin duda alguna.
Ejecuté mi punto de referencia en dos máquinas (core i5, core i7) y vi que memmove es en realidad más rápido que memcpy, en el núcleo i7 más antiguo ¡incluso casi el doble de rápido! Ahora busco explicaciones.
Aquí está mi punto de referencia. Copia 100 mb con memcpy y luego mueve alrededor de 100 mb con memmove; el origen y el destino se superponen. Se prueban varias "distancias" para origen y destino. Cada prueba se ejecuta 10 veces, se imprime el tiempo promedio.
https://gist.github.com/cruppstahl/78a57cdf937bca3d062c
Aquí están los resultados en el Core i5 (Linux 3.5.0-54-generic # 81 ~ precisa1-Ubuntu SMP x86_64 GNU / Linux, gcc es 4.6.3 (Ubuntu / Linaro 4.6.3-1ubuntu5). El número entre paréntesis es la distancia (tamaño del espacio) entre el origen y el destino:
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 se implementa como un código ensamblador optimizado SSE, copiando de atrás hacia adelante. Utiliza la captación previa de hardware para cargar los datos en la caché, copia 128 bytes en registros XMM y luego los almacena en el destino.
( memcpy-ssse3-back . S , líneas 1650 y siguientes)
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)
¿Por qué memmove es más rápido que memcpy? Esperaría que memcpy copiara páginas de memoria, lo que debería ser mucho más rápido que el bucle. En el peor de los casos, esperaría que memcpy fuera tan rápido como memmove.
PD: Sé que no puedo reemplazar memmove con memcpy en mi código. Sé que el ejemplo de código mezcla C y C ++. Esta pregunta es realmente solo para fines académicos.
ACTUALIZACIÓN 1
Ejecuté algunas variaciones de las pruebas, en función de las diversas respuestas.
- Cuando se ejecuta memcpy dos veces, la segunda ejecución es más rápida que la primera.
- Al "tocar" el búfer de destino de memcpy (
memset(b2, 0, BUFFERSIZE...)
), la primera ejecución de memcpy también es más rápida. - memcpy sigue siendo un poco más lento que memmove.
Aquí están los resultados:
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
Mi conclusión: según un comentario de @Oliver Charlesworth, el sistema operativo tiene que comprometer la memoria física tan pronto como se accede al búfer de destino de memcpy por primera vez (si alguien sabe cómo "probar" esto, ¡agregue una respuesta! ). Además, como dijo @Mats Petersson, memmove es más amigable con la caché que memcpy.
¡Gracias por todas las excelentes respuestas y comentarios!