windows - ejemplo - memset c
Diferencias de rendimiento de memcpy entre procesos de 32 y 64 bits (7)
Aquí hay un ejemplo de una rutina memcpy orientada específicamente para la arquitectura de 64 bits.
void uint8copy(void *dest, void *src, size_t n){
uint64_t * ss = (uint64_t)src;
uint64_t * dd = (uint64_t)dest;
n = n * sizeof(uint8_t)/sizeof(uint64_t);
while(n--)
*dd++ = *ss++;
}//end uint8copy()
El artículo completo está aquí: http://www.godlikemouse.com/2008/03/04/optimizing-memcpy-routines/
Tenemos máquinas Core2 (Dell T5400) con XP64.
Observamos que cuando se ejecutan procesos de 32 bits, el rendimiento de memcpy es del orden de 1.2GByte / s; sin embargo, memcpy en un proceso de 64 bits logra aproximadamente 2.2GByte / s (o 2.4GByte / s con el memcpy del compilador Intel CRT). Si bien la reacción inicial podría ser simplemente explicar esto como debido a los registros más amplios disponibles en el código de 64 bits, observamos que nuestro propio código ensamblador SSE similar a memcpy (que debería usar almacenes de carga de 128 bits independientemente de 32 / 64 bitness del proceso) demuestra límites superiores similares en el ancho de banda de la copia que logra.
Mi pregunta es, ¿a qué se debe esta diferencia? ¿Los procesos de 32 bits tienen que pasar por algunos aros WOW64 adicionales para llegar a la RAM? ¿Tiene algo que ver con TLB o precaptores o ... qué?
Gracias por cualquier idea.
También se planteó en foros de Intel .
Creo que lo siguiente puede explicarlo:
Para copiar datos de la memoria a un registro y volver a la memoria, lo hace
mov eax, [address]
mov [address2], eax
Esto mueve 32 bit (4 byte) de la dirección a la dirección2. Lo mismo ocurre con 64 bits en el modo de 64 bits
mov rax, [address]
mov [address2], rax
Esto mueve 64 bit, 2 byte, de la dirección a la dirección2. "mov" sí mismo, independientemente de si es de 64 bits o 32 bits tiene una latencia de 0.5 y un rendimiento de 0.5 según las especificaciones de Intel. La latencia es cuántos ciclos de reloj requiere la instrucción para viajar a través de la canalización y el rendimiento es cuánto tiempo debe esperar la CPU antes de volver a aceptar la misma instrucción. Como puede ver, puede hacer dos movimientos por ciclo de reloj, sin embargo, tiene que esperar medio ciclo de reloj entre dos movimientos, por lo que solo puede hacer un movimiento por ciclo de reloj (¿o estoy equivocado aquí y malinterpretar los términos? Ver PDF aquí para más detalles).
Por supuesto, un mov reg, mem
puede ser más largo que 0.5 ciclos, dependiendo de si los datos están en caché de primer o segundo nivel, o no están en la memoria caché y deben ser tomados de la memoria. Sin embargo, el tiempo de latencia anterior ignora este hecho (como el PDF indica que he vinculado anteriormente), supone que todos los datos necesarios para el movimiento ya están presentes (de lo contrario, la latencia aumentará según el tiempo necesario para obtener los datos desde donde sea ahora mismo - esto podría ser en varios ciclos de reloj y es completamente independiente del comando que se está ejecutando, dice el PDF en la página 482 / C-30).
Lo que es interesante, si el mov es 32 o 64 bits no juega ningún papel. Eso significa que a menos que el ancho de banda de la memoria se convierta en el factor limitante, los de 64 bits son igualmente rápidos a los de 32 bits de mov, y como solo la mitad de los mov mueve la misma cantidad de datos de A a B al usar 64 bits, el rendimiento puede (en teoría) es dos veces más alto (el hecho de que no lo es es probablemente porque la memoria no es ilimitada rápidamente).
De acuerdo, ahora piensas que cuando usas los registros SSE más grandes, debes obtener un rendimiento más rápido, ¿verdad? AFAIK los registros xmm no son 256, sino 128 bits de ancho, por cierto ( referencia en Wikipedia ). Sin embargo, ¿ha considerado la latencia y el rendimiento? O bien los datos que desea mover están alineados a 128 bits o no. Dependiendo de eso, o lo mueve usando
movdqa xmm1, [address]
movdqa [address2], xmm1
o si no está alineado
movdqu xmm1, [address]
movdqu [address2], xmm1
Bueno, movdqa / movdqu tiene una latencia de 1 y un rendimiento de 1. Por lo tanto, las instrucciones tardan el doble en ejecutarse y el tiempo de espera después de las instrucciones es dos veces más largo que un movimiento normal.
Y algo más que no hemos tenido en cuenta es el hecho de que la CPU realmente divide las instrucciones en microoperaciones y puede ejecutarlas en paralelo. Ahora comienza a ser realmente complicado ... incluso demasiado complicado para mí.
De todos modos, sé por experiencia que cargar datos a / desde registros xmm es mucho más lento que cargar datos a / desde registros normales, por lo que su idea de acelerar la transferencia usando registros xmm estaba condenada desde el primer segundo. De hecho, me sorprende que al final el memmove SSE no sea mucho más lento que el normal.
Finalmente llegué al fondo de esto (y Die en la respuesta de Sente estaba en la línea correcta, gracias)
En el siguiente, dst y src son 512 MByte std :: vector. Estoy usando el compilador Intel 10.1.029 y CRT.
En 64bit ambos
memcpy(&dst[0],&src[0],dst.size())
y
memcpy(&dst[0],&src[0],N)
donde N se declaró previamente const size_t N=512*(1<<20);
llamada
__intel_fast_memcpy
la mayor parte de lo cual consiste en:
000000014004ED80 lea rcx,[rcx+40h]
000000014004ED84 lea rdx,[rdx+40h]
000000014004ED88 lea r8,[r8-40h]
000000014004ED8C prefetchnta [rdx+180h]
000000014004ED93 movdqu xmm0,xmmword ptr [rdx-40h]
000000014004ED98 movdqu xmm1,xmmword ptr [rdx-30h]
000000014004ED9D cmp r8,40h
000000014004EDA1 movntdq xmmword ptr [rcx-40h],xmm0
000000014004EDA6 movntdq xmmword ptr [rcx-30h],xmm1
000000014004EDAB movdqu xmm2,xmmword ptr [rdx-20h]
000000014004EDB0 movdqu xmm3,xmmword ptr [rdx-10h]
000000014004EDB5 movntdq xmmword ptr [rcx-20h],xmm2
000000014004EDBA movntdq xmmword ptr [rcx-10h],xmm3
000000014004EDBF jge 000000014004ED80
y funciona a ~ 2200 MByte / s.
Pero en 32 bits
memcpy(&dst[0],&src[0],dst.size())
llamadas
__intel_fast_memcpy
la mayor parte del cual consiste en
004447A0 sub ecx,80h
004447A6 movdqa xmm0,xmmword ptr [esi]
004447AA movdqa xmm1,xmmword ptr [esi+10h]
004447AF movdqa xmmword ptr [edx],xmm0
004447B3 movdqa xmmword ptr [edx+10h],xmm1
004447B8 movdqa xmm2,xmmword ptr [esi+20h]
004447BD movdqa xmm3,xmmword ptr [esi+30h]
004447C2 movdqa xmmword ptr [edx+20h],xmm2
004447C7 movdqa xmmword ptr [edx+30h],xmm3
004447CC movdqa xmm4,xmmword ptr [esi+40h]
004447D1 movdqa xmm5,xmmword ptr [esi+50h]
004447D6 movdqa xmmword ptr [edx+40h],xmm4
004447DB movdqa xmmword ptr [edx+50h],xmm5
004447E0 movdqa xmm6,xmmword ptr [esi+60h]
004447E5 movdqa xmm7,xmmword ptr [esi+70h]
004447EA add esi,80h
004447F0 movdqa xmmword ptr [edx+60h],xmm6
004447F5 movdqa xmmword ptr [edx+70h],xmm7
004447FA add edx,80h
00444800 cmp ecx,80h
00444806 jge 004447A0
y funciona a ~ 1350 MByte / s solamente.
SIN EMBARGO
memcpy(&dst[0],&src[0],N)
donde N se declaró previamente const size_t N=512*(1<<20);
compila (en 32 bits) a una llamada directa a un
__intel_VEC_memcpy
la mayor parte del cual consiste en
0043FF40 movdqa xmm0,xmmword ptr [esi]
0043FF44 movdqa xmm1,xmmword ptr [esi+10h]
0043FF49 movdqa xmm2,xmmword ptr [esi+20h]
0043FF4E movdqa xmm3,xmmword ptr [esi+30h]
0043FF53 movntdq xmmword ptr [edi],xmm0
0043FF57 movntdq xmmword ptr [edi+10h],xmm1
0043FF5C movntdq xmmword ptr [edi+20h],xmm2
0043FF61 movntdq xmmword ptr [edi+30h],xmm3
0043FF66 movdqa xmm4,xmmword ptr [esi+40h]
0043FF6B movdqa xmm5,xmmword ptr [esi+50h]
0043FF70 movdqa xmm6,xmmword ptr [esi+60h]
0043FF75 movdqa xmm7,xmmword ptr [esi+70h]
0043FF7A movntdq xmmword ptr [edi+40h],xmm4
0043FF7F movntdq xmmword ptr [edi+50h],xmm5
0043FF84 movntdq xmmword ptr [edi+60h],xmm6
0043FF89 movntdq xmmword ptr [edi+70h],xmm7
0043FF8E lea esi,[esi+80h]
0043FF94 lea edi,[edi+80h]
0043FF9A dec ecx
0043FF9B jne ___intel_VEC_memcpy+244h (43FF40h)
y se ejecuta a ~ 2100MByte / s (y probar 32 bits no es de alguna manera limitado al ancho de banda).
Retiro mi reclamo de que mi propio código SSE similar a memcpy sufre de un ~ 1300 MByte / limit similar en compilaciones de 32 bits; Ahora no tengo ningún problema para obtener> 2GByte / s en 32 o 64 bits; el truco (como lo sugiere la sugerencia de los resultados anteriores) es utilizar tiendas no temporales ("transmisión") (por ejemplo, _mm_stream_ps
intrínseco).
Parece un poco extraño que la dst.size()
" dst.size()
" de 32 bits finalmente no llame a la versión más rápida " movnt
" (si ingresas a memcpy existe la cantidad más increíble de comprobación de CPUID
y lógica heurística, por ejemplo, comparando el número de bytes para copiar con el tamaño de caché, etc., antes de que se acerque a tus datos reales), pero al menos ya entiendo el comportamiento observado (y no está relacionado con SysWow64 o H / W).
Gracias por los comentarios positivos! Creo que puedo explicar en parte lo que está pasando aquí.
Usar las tiendas no temporales para memcpy es definitivamente el ayuno si solo estás cronometrando la llamada a memcpy.
Por otro lado, si está comparando una aplicación, las tiendas movdqa tienen la ventaja de que dejan la memoria de destino en caché. O al menos la parte que encaja en el caché.
Entonces, si está diseñando una biblioteca en tiempo de ejecución y puede suponer que la aplicación que llamó a memcpy va a utilizar el búfer de destino inmediatamente después de la llamada a memcpy, entonces querrá proporcionar la versión de movdqa. Esto efectivamente optimiza el viaje desde la memoria a la CPU que seguiría a la versión movntdq, y todas las instrucciones que siguen a la llamada se ejecutarán más rápido.
Pero, por otro lado, si el búfer de destino es grande en comparación con el caché del procesador, esa optimización no funciona y la versión de movntdq le daría puntos de referencia de la aplicación más rápidos.
Entonces, la idea de memcpy tendría múltiples versiones bajo el capó. Cuando el búfer de destino es pequeño en comparación con el caché del procesador, utilice movdqa, de lo contrario, el búfer de destino es grande en comparación con el caché del procesador, use movntdq. Parece que esto es lo que está sucediendo en la biblioteca de 32 bits.
Por supuesto, nada de esto tiene nada que ver con las diferencias entre 32 bits y 64 bits.
Mi conjetura es que la biblioteca de 64 bits no es tan madura. Los desarrolladores aún no se han dado a la tarea de proporcionar ambas rutinas en esa versión de la biblioteca.
Mi suposición imprevista es que los procesos de 64 bits utilizan el tamaño de la memoria nativa de 64 bits del procesador, lo que optimiza el uso del bus de memoria.
No tengo una referencia delante de mí, así que no estoy absolutamente seguro de los tiempos / instrucciones, pero aún puedo dar la teoría. Si está haciendo un movimiento de memoria en el modo de 32 bits, hará algo así como un "rep movsd" que mueve un solo valor de 32 bits en cada ciclo de reloj. En el modo de 64 bits, puede hacer un "rep movsq" que hace un solo movimiento de 64 bits cada ciclo de reloj. Esa instrucción no está disponible para el código de 32 bits, por lo que estaría haciendo 2 x rep movsd (en 1 ciclo por pieza) para la mitad de la velocidad de ejecución.
MUY simplificado, ignorando todos los problemas de ancho de banda / alineamiento de memoria, etc., pero aquí es donde comienza todo ...
Por supuesto, realmente necesita ver las instrucciones reales de la máquina que se están ejecutando dentro del bucle más interno de memcpy, entrando en el código de máquina con un depurador. Cualquier otra cosa es solo especulación.
Mi pregunta es que probablemente no tiene nada que ver con 32 bits versus 64 bits per se; mi suposición es que la rutina de la biblioteca más rápida se escribió usando almacenes no temporales SSE.
Si el bucle interno contiene alguna variación de las instrucciones de la tienda de carga convencional, entonces la memoria de destino debe leerse en la memoria caché de la máquina, modificarse y escribirse de nuevo. Como esa lectura es totalmente innecesaria, los bits que se leen se sobrescriben inmediatamente, puede guardar la mitad del ancho de banda de la memoria utilizando las instrucciones de escritura "no temporales", que omiten las cachés. De esta forma, la memoria de destino se acaba de escribir haciendo un viaje de ida a la memoria en lugar de un viaje de ida y vuelta.
No conozco la biblioteca CRT del compilador de Intel, así que esto es solo una suposición. No hay una razón particular por la que el libCRT de 32 bits no pueda hacer lo mismo, pero la aceleración que citan está en el estadio de lo que esperaría simplemente al convertir las instrucciones movdqa a movnt ...
Como memcpy no hace ningún cálculo, siempre está vinculado por la velocidad con la que puede leer y escribir en la memoria.