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 .

¿Fue útil?

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 /

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top