diferencias de rendimiento de memcpy entre procesos de 32 y 64 bits
Pregunta
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 alcanza alrededor de 2.2GByte / s (o 2.4GByte / s con la compilación Intel CRT's memcpy). Mientras que la la reacción inicial podría ser simplemente explicar esto lejos debido a los registros más amplios disponibles en código de 64 bits, observamos que nuestro propio memcpy-like Código de ensamblaje SSE (que debería estar usando 128 bits amplios almacenes de carga, independientemente de la 32/64 bits de el proceso) demuestra límites superiores similares en el ancho de banda de copia que logra.
Mi pregunta es, ¿cuál es realmente esta diferencia? debido a ? ¿Los procesos de 32 bits tienen que saltar algunos aros WOW64 adicionales para llegar a la RAM? Es algo que ver con TLBs o prefetchers o ... ¿qué?
Gracias por cualquier idea.
También se planteó en foros de Intel .
Solución
Por supuesto, realmente necesita mirar las instrucciones reales de la máquina que se ejecutan dentro del bucle más interno de la memoria, entrando en el código de la máquina con un depurador. Cualquier otra cosa es solo especulación.
Mi pregunta es que probablemente no tenga nada que ver con 32 bits versus 64 bits per se; Supongo que la rutina de biblioteca más rápida se escribió utilizando almacenes no temporales SSE.
Si el bucle interno contiene alguna variación de las instrucciones convencionales de almacenamiento de carga, entonces la memoria de destino debe leerse en la memoria caché de la máquina, modificarse y volverse a escribir. Dado que esa lectura es totalmente innecesaria (los bits que se leen se sobrescriben inmediatamente), puede ahorrar la mitad del ancho de banda de la memoria utilizando & Quot; no temporal & Quot; escribir instrucciones, que omiten los cachés. De esa manera, la memoria de destino se escribe haciendo un viaje de ida a la memoria en lugar de un viaje de ida y vuelta.
No conozco la biblioteca CRT del compilador Intel, así que esto es solo una suposición. No hay una razón particular por la cual el libCRT de 32 bits no pueda hacer lo mismo, pero la aceleración que usted cita está en el estadio de lo que esperaría simplemente convirtiendo las instrucciones movdqa a movnt ...
Dado que memcpy no está haciendo ningún cálculo, siempre está sujeto a la rapidez con que puede leer y escribir memoria.
Otros consejos
Creo que lo siguiente puede explicarlo:
Para copiar datos de la memoria a un registro y volver a la memoria, debe hacerlo
mov eax, [address]
mov [address2], eax
Esto mueve 32 bits (4 bytes) de una dirección a otra2. Lo mismo ocurre con 64 bits en modo de 64 bits
mov rax, [address]
mov [address2], rax
Esto mueve 64 bits, 2 bytes, de una dirección a otra2. " mov " en sí mismo, independientemente de si es de 64 bits o de 32 bits, tiene una latencia de 0.5 y un rendimiento de 0.5 de acuerdo con las especificaciones de Intel. La latencia es cuántos ciclos de reloj tarda la instrucción en recorrer la tubería y el rendimiento es el tiempo que la CPU tiene que esperar antes de aceptar nuevamente la misma instrucción. Como puede ver, puede hacer dos movimientos por ciclo de reloj, sin embargo, tiene que esperar medio ciclo entre dos movimientos, por lo que efectivamente solo puede hacer un movimiento por ciclo de reloj (¿o me equivoco aquí y malinterpreto los términos? Consulte PDF aquí para obtener más detalles).
Por supuesto, un mov reg, mem
puede durar más de 0.5 ciclos, dependiendo de si los datos están en el caché de primer o segundo nivel, o no están en caché en absoluto y necesitan ser tomados de la memoria. Sin embargo, el tiempo de latencia de arriba ignora este hecho (como el PDF indica que he vinculado anteriormente), se supone que todos los datos necesarios para el movimiento ya están presentes (de lo contrario, la latencia aumentará según el tiempo que lleve obtener los datos desde donde sea que esté en este momento, esto puede ser varios ciclos de reloj y es completamente independiente del comando que se ejecuta, dice el PDF en la página 482 / C-30).
Lo que es interesante, si el mov es de 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 movimientos de 64 bits son igualmente rápidos a los de 32 bits, y dado que solo se necesita la mitad de los movimientos para mover la misma cantidad de datos de A a B cuando se usan 64 bits, el rendimiento puede (en teoría) sea el doble (el hecho de que no lo sea probablemente porque la memoria no es ilimitada rápidamente).
Bien, ahora piensa que cuando usa los registros SSE más grandes, debería 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? Los datos que desea mover están alineados o no a 128 bits. Dependiendo de eso, puedes moverlo 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 el doble que un mov normal.
Y algo más que ni siquiera hemos tenido en cuenta es el hecho de que la CPU 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 mediante el uso de registros xmm estaba condenada desde el primer segundo. De hecho, estoy sorprendido de que al final el SSE memmove no sea mucho más lento que el normal.
Finalmente llegué al fondo de esto (y la respuesta de Die in 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 64 bits ambos
memcpy(&dst[0],&src[0],dst.size())
y
memcpy(&dst[0],&src[0],N)
donde N se declara previamente const size_t N=512*(1<<20);
llamar
__intel_fast_memcpy
la mayor parte de los cuales 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 se ejecuta a ~ 2200 MByte / s.
Pero en 32 bits
_mm_stream_ps
llamadas
dst.size()
la mayor parte de la 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 se ejecuta a ~ 1350 MByte / s solamente.
HOWEVER
memcpy(&dst[0],&src[0],N)
donde N se declara previamente movnt
compila (en 32 bits) a una llamada directa a un
__intel_VEC_memcpy
la mayor parte de la 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 ~ 2100 MByte / s (y probar que 32 bits no tiene de alguna manera un ancho de banda limitado).
Retiro mi reclamo de que mi propio código SSE tipo memcpy sufre de un
similar ~ 1300 MByte / límite en versiones de 32 bits; Ahora no tengo ningún problema
obteniendo > 2GByte / s en 32 o 64bit; el truco (como lo indica el resultado anterior)
es usar tiendas no temporales (" streaming ") (por ejemplo, CPUID
intrínsecas).
Parece un poco extraño que el 32bit " <=> " memcpy finalmente no llame al " <=> " versión (si entras en memcpy hay más increíble cantidad de <=> comprobación y lógica heurística, por ejemplo, comparar números de bytes que se copiarán con el tamaño de caché, etc., antes de que se acerque a su datos reales) pero al menos ahora entiendo el comportamiento observado (y es no SysWow64 o H / W relacionados).
Mi conjetura es que los procesos de 64 bits están utilizando el tamaño de memoria de 64 bits nativo del procesador, lo que optimiza el uso del bus de memoria.
¡Gracias por los comentarios positivos! Creo que puedo en parte explicar lo que está pasando aquí.
Usar los almacenes no temporales para memcpy es definitivamente el if en ayunas que solo está cronometrando la llamada de 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 cabe en el caché.
Entonces, si está diseñando una biblioteca en tiempo de ejecución y si puede suponer que la aplicación que llamó a memcpy va a usar el búfer de destino inmediatamente después de la llamada de memcpy, entonces querrá proporcionar la versión movdqa. Esto optimiza efectivamente el viaje desde la memoria de regreso 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 movntdq le proporcionaría puntos de referencia de aplicación más rápidos.
Entonces, la idea 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, use 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 simplemente no es tan madura. Los desarrolladores aún no han podido proporcionar ambas rutinas en esa versión de la biblioteca todavía.
No tengo una referencia frente a mí, por lo que no soy absolutamente positivo sobre los tiempos / instrucciones, pero aún puedo dar la teoría. Si está haciendo un movimiento de memoria en modo de 32 bits, hará algo 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 & Quot; rep movsq & Quot; que hace un solo movimiento de 64 bits en 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 (a 1 ciclo por pieza) por la mitad de la velocidad de ejecución.
MUY simplificado, ignorando todos los problemas de ancho de banda de memoria / alineación, etc., pero aquí es donde todo comienza ...
Aquí hay un ejemplo de una rutina memcpy diseñada 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-rutines /