2008-11-06 10 views
9

Tenemos máquinas Core2 (Dell T5400) con XP64.memcpy diferencias de rendimiento entre procesos de 32 y 64 bits

Observamos que al ejecutar 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.2 GByte/s (o 2.4 GByte/s con el compilador Intel CRT memcpy). Mientras que la reacción inicial de podría ser solo explicar esto ya que debido a los registros más amplios disponibles en código de 64 bits, observamos que nuestro propio código de ensamblaje SSE de memcpy (que debería usar 128-bit de carga ancha -tiendas independientemente de 32/64-bitness de el proceso) demuestra límites superiores similares en el ancho de banda de la copia que logra.

Mi pregunta es, ¿en qué se diferencia realmente debido a? ¿Los procesos de 32 bits deben pasar por algunos aros WOW64 extra para llegar a la RAM? ¿Es algo que hacer con TLB o prebuscador o ... qué?

Gracias por cualquier idea.

También se menciona en Intel forums.

+0

¿Quiere decir que su código de SSE es también el doble de rápido en el modo de 64 bits nativa que en WOW64? ¿Lo ha comparado con XP de 32 bits para ver si WOW64 está afectando el rendimiento? – bk1e

+0

Sí, eso es exactamente. La prueba del sistema operativo de 32 bits es una sugerencia excelente ... ¡pero desafortunadamente no tenemos ningún H/W equivalente con sistema operativo de 32 bits! Esperaba que alguien me dijera si WOW64 es el problema o no. Se buscará obtener una instalación de 32 bits. – timday

Respuesta

3

Por supuesto, que realmente necesita para mirar las instrucciones reales de la máquina que se están ejecutando dentro del bucle más interno de la memcpy, 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 tiene nada que ver con 32 bits frente a 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 ninguna razón particular por la cual el libCRT de 32 bits no puede hacer lo mismo, pero la aceleración que usted cita está en el estadio de lo que esperaría simplemente al convertir las instrucciones movdqa a movnt ...

Dado que memcpy es sin hacer ningún cálculo, siempre está vinculado por la velocidad con la que puede leer y escribir en la memoria.

+0

Yup resulta que tenía razón sobre las tiendas no temporales. Ver mi respuesta para los detalles del nivel de asma gnarly. El problema fundamental parece ser que el compilador/CRT de Intel no siempre usa su versión no temporal de memcpy en 32 bits. – timday

1

Supongo que los procesos de 64 bits utilizan el tamaño de la memoria de 64 bits nativa del procesador, lo que optimiza el uso del bus de memoria.

8

creo que el siguiente puede explicarlo:

Para copiar datos desde la memoria a un registro y de nuevo a la memoria, que hacen

mov eax, [address] 
mov [address2], eax 

Esto mueve 32 bits (4 bytes) de la dirección de address2 . Lo mismo ocurre con el modo de bits de 64 bits en 64

mov rax, [address] 
mov [address2], rax 

Esto mueve 64 bits, de 2 bytes, desde la dirección de address2. "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 y malinterpretar los términos? Ver PDF here para 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 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 cree que al usar los registros SSE más grandes, debería obtener un rendimiento más rápido, ¿no? AFAIK los registros xmm no son 256, sino 128 bits de ancho, BTW (reference at 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. En función de eso, ya sea moverlo usando

movdqa xmm1, [address] 
movdqa [address2], xmm1 

o si no se encuadra

movdqu xmm1, [address] 
movdqu [address2], xmm1 

Bueno, movdqa/movdqu tiene una latencia de 1 y un rendimiento de 1. Así, las instrucciones de tomar el doble de tiempo para ser ejecutado y el tiempo de espera después de las instrucciones es dos veces más largo que un mov 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.

+0

Muy bien escrito, lo entendí y no sé mucho sobre cómo funcionan realmente los procesadores. – cfeduke

+0

Bueno, todo esto está muy bien (gracias por la corrección del ancho SSE) pero en realidad no responde la pregunta básica: ¿por qué el código que simplemente satura el ancho de banda de la memoria funciona mucho mejor en 64 bits nativos que en 32 bits en WOW64? ¿Dónde está el cuello de botella? – timday

0

No tengo una referencia delante de mí, así que no estoy absolutamente seguro de los tiempos/instrucciones, pero todavía 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.

mucho simplificada, haciendo caso omiso de todos los problemas de ancho de banda de memoria/alineación, etc, pero aquí es donde empieza todo ...

+0

Pero eso no explica por qué la copia de código a través de registros SSE (que son de 128 bits si está en el modo de 32 bits o de 64 bits) parece ser de ancho de banda limitado en 32 bits. – timday

+0

Los registros de SSE deberían estar almacenados en el ancho del bus de datos (64 bits). Sin embargo, dado que no tengo los tiempos en frente de mí, las tiendas de SSE podrían usar el doble de los ciclos de reloj de una tienda de registros normal y, por lo tanto, tener la misma velocidad de datos que una copia de 32 bits. –

5

por fin llegué a la parte inferior de esta (y morir en la respuesta de Sente estaba en la dirección correcta, gracias)

En el siguiente, el horario de verano y src son 512 MByte std :: vector. Estoy usando el compilador Intel 10.1.029 y CRT.

En 64 bits tanto

memcpy(&dst[0],&src[0],dst.size())

y

memcpy(&dst[0],&src[0],N)

donde N es declarado previamente const size_t N=512*(1<<20); llamada

__intel_fast_memcpy

la mayor parte de los cuales se compone de:

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 ~ 2,200 Mbytes/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 es declarado previamente const size_t N=512*(1<<20); compila (en 32 bits) a una llamada directa a un

__intel_VEC_memcpy 

la mayor parte de los cuales consta de

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 funciona a ~ 2100MByte/s (y prueba 32bit de alguna manera no es ancho de banda).

Retiro mi afirmación 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 obteniendo> 2GByte/s en 32 o 64 bits; el truco (como lo indica la sugerencia de los resultados anteriores) es usar almacenes no temporales ("transmisión") (por ejemplo, _mm_stream_ps intrínseco).

Me parece un poco extraño que el 32 bits "dst.size()" memcpy hace finalmente no llamada la versión más rápida "movnt" (si es que paso en establecimiento de memoria no es el más increíble cantidad de CPUID de cheques y heurística lógica por ejemplo, número comparar de bytes que se copiarán con el tamaño de caché, etc. antes de que se acerque a los datos reales de ), pero al menos ya entiendo el comportamiento observado (y es no relacionado con SysWow64 o H/W).

1

¡Gracias por los comentarios positivos! Creo que puedo en parte explicar lo que está pasando aquí.

Usando las tiendas no temporales de establecimiento de memoria es sin duda el ayuno si que sólo está cronometrando el establecimiento de memoria llamada.

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é.

Así que si está diseñando una biblioteca de 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, 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.

Así que 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 simplemente 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.

+0

Sí, toda la cuestión de qué estado quiere que el caché en post-copia sea interesante. Estaba usando> 256 MB de copias. Si copio algo más comparable con el tamaño de la memoria caché, veo que todas las memcpy que he visto revierten de manera sensata desde tiendas de transmisión (no temporales) a movimientos convencionales. – timday

0

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/

+0

Eso está muy bien, pero si lo comparas con un x86 moderno contra una buena memcpy usando las llamadas tiendas no temporales (por ejemplo, la que se proporciona en el compilador Intel CRT), la tuya será más lenta. – timday

+2

Por cierto, el sitio web es muy llamativo, pero si va a escribir artículos creíbles sobre optimización, necesita comparar las alternativas y dar algunos resultados de tiempo cuantitativos para cada una como evidencia de que un enfoque particular es mejor. Claramente eres capaz de hacer esto (por ejemplo, tu artículo sobre rendimiento de escritura de archivos); Te recomiendo que revises tu artículo y al menos compares el rendimiento de tu código con el memcpy de tu sistema para empezar. – timday

Cuestiones relacionadas