2010-10-27 11 views
9

Estoy compilando un poco de código usando los siguientes ajustes en VC++ 2010:/O2/Ob2/Oi/Ot¿Qué está haciendo mi compilador? (Memcpy optimización)

Sin embargo estoy teniendo algunos problemas para entender algunas partes del conjunto generada , He puesto algunas preguntas en el código como comentarios.

Además, ¿qué distancia de precaptura generalmente se recomienda en la CPU moderna? No puedo probar en mi propia CPU, pero esperaba algún valor que funcione bien en una gama más amplia de CPU. ¿Tal vez uno podría usar distancias dinámicas de captación previa?

< --edit:

Otra cosa que me sorprende es de que el compilador no intercalar en alguna forma las instrucciones movdqa y movntdq? Dado que estas instrucciones son en cierto sentido asincrónicas desde mi comprensión.

Este código también asume líneas de caché de 32 bytes cuando se realiza la búsqueda previa, sin embargo, parece que la CPU de gama alta tiene caché de 64 bytes, por lo que es probable que se puedan eliminar 2 de las búsquedas previas.

->

void memcpy_aligned_x86(void* dest, const void* source, size_t size) 
{ 
0052AC20 push  ebp 
0052AC21 mov   ebp,esp 
const __m128i* source_128 = reinterpret_cast<const __m128i*>(source); 

for(size_t n = 0; n < size/16; n += 8) 
0052AC23 mov   edx,dword ptr [size] 
0052AC26 mov   ecx,dword ptr [dest] 
0052AC29 mov   eax,dword ptr [source] 
0052AC2C shr   edx,4 
0052AC2F test  edx,edx 
0052AC31 je   copy+9Eh (52ACBEh) 
__m128i xmm0 = _mm_setzero_si128(); 
__m128i xmm1 = _mm_setzero_si128(); 
__m128i xmm2 = _mm_setzero_si128(); 
__m128i xmm3 = _mm_setzero_si128(); 
__m128i xmm4 = _mm_setzero_si128(); 
__m128i xmm5 = _mm_setzero_si128(); 
__m128i xmm6 = _mm_setzero_si128(); 
__m128i xmm7 = _mm_setzero_si128(); 

__m128i* dest_128 = reinterpret_cast<__m128i*>(dest); 
0052AC37 push  esi 
0052AC38 push  edi 
0052AC39 lea   edi,[edx-1] 
0052AC3C shr   edi,3 
0052AC3F inc   edi 
{ 
    _mm_prefetch(reinterpret_cast<const char*>(source_128+8), _MM_HINT_NTA); 
    _mm_prefetch(reinterpret_cast<const char*>(source_128+10), _MM_HINT_NTA); 
    _mm_prefetch(reinterpret_cast<const char*>(source_128+12), _MM_HINT_NTA); 
    _mm_prefetch(reinterpret_cast<const char*>(source_128+14), _MM_HINT_NTA); 

    xmm0 = _mm_load_si128(source_128++); 
    xmm1 = _mm_load_si128(source_128++); 
    xmm2 = _mm_load_si128(source_128++); 
    xmm3 = _mm_load_si128(source_128++); 
    xmm4 = _mm_load_si128(source_128++); 
    xmm5 = _mm_load_si128(source_128++); 
    xmm6 = _mm_load_si128(source_128++); 
    xmm7 = _mm_load_si128(source_128++); 
0052AC40 movdqa  xmm6,xmmword ptr [eax+70h] // 1. Why is this moved before the pretecthes? 
0052AC45 prefetchnta [eax+80h] 
0052AC4C prefetchnta [eax+0A0h] 
0052AC53 prefetchnta [eax+0C0h] 
0052AC5A prefetchnta [eax+0E0h] 
0052AC61 movdqa  xmm0,xmmword ptr [eax+10h] 
0052AC66 movdqa  xmm1,xmmword ptr [eax+20h] 
0052AC6B movdqa  xmm2,xmmword ptr [eax+30h] 
0052AC70 movdqa  xmm3,xmmword ptr [eax+40h] 
0052AC75 movdqa  xmm4,xmmword ptr [eax+50h] 
0052AC7A movdqa  xmm5,xmmword ptr [eax+60h] 
0052AC7F lea   esi,[eax+70h] // 2. What is happening in these 2 lines? 
0052AC82 mov   edx,eax  // 
0052AC84 movdqa  xmm7,xmmword ptr [edx] // 3. Why edx? and not simply eax? 

    _mm_stream_si128(dest_128++, xmm0); 
0052AC88 mov   esi,ecx // 4. Is esi never used? 
0052AC8A movntdq  xmmword ptr [esi],xmm7 
    _mm_stream_si128(dest_128++, xmm1); 
0052AC8E movntdq  xmmword ptr [ecx+10h],xmm0 
    _mm_stream_si128(dest_128++, xmm2); 
0052AC93 movntdq  xmmword ptr [ecx+20h],xmm1 
    _mm_stream_si128(dest_128++, xmm3); 
0052AC98 movntdq  xmmword ptr [ecx+30h],xmm2 
    _mm_stream_si128(dest_128++, xmm4); 
0052AC9D movntdq  xmmword ptr [ecx+40h],xmm3 
    _mm_stream_si128(dest_128++, xmm5); 
0052ACA2 movntdq  xmmword ptr [ecx+50h],xmm4 
    _mm_stream_si128(dest_128++, xmm6); 
0052ACA7 movntdq  xmmword ptr [ecx+60h],xmm5 
    _mm_stream_si128(dest_128++, xmm7); 
0052ACAC lea   edx,[ecx+70h] 
0052ACAF sub   eax,0FFFFFF80h 
0052ACB2 sub   ecx,0FFFFFF80h 
0052ACB5 dec   edi 
0052ACB6 movntdq  xmmword ptr [edx],xmm6 // 5. Why not simply ecx? 
0052ACBA jne   copy+20h (52AC40h) 
0052ACBC pop   edi 
0052ACBD pop   esi 
} 
} 

código original:

void memcpy_aligned_x86(void* dest, const void* source, size_t size) 
{ 
assert(dest != nullptr); 
assert(source != nullptr); 
assert(source != dest); 
assert(size % 128 == 0); 

__m128i xmm0 = _mm_setzero_si128(); 
__m128i xmm1 = _mm_setzero_si128(); 
__m128i xmm2 = _mm_setzero_si128(); 
__m128i xmm3 = _mm_setzero_si128(); 
__m128i xmm4 = _mm_setzero_si128(); 
__m128i xmm5 = _mm_setzero_si128(); 
__m128i xmm6 = _mm_setzero_si128(); 
__m128i xmm7 = _mm_setzero_si128(); 

__m128i* dest_128 = reinterpret_cast<__m128i*>(dest); 
const __m128i* source_128 = reinterpret_cast<const __m128i*>(source); 

for(size_t n = 0; n < size/16; n += 8) 
{ 
    _mm_prefetch(reinterpret_cast<const char*>(source_128+8), _MM_HINT_NTA); 
    _mm_prefetch(reinterpret_cast<const char*>(source_128+10), _MM_HINT_NTA); 
    _mm_prefetch(reinterpret_cast<const char*>(source_128+12), _MM_HINT_NTA); 
    _mm_prefetch(reinterpret_cast<const char*>(source_128+14), _MM_HINT_NTA); 

    xmm0 = _mm_load_si128(source_128++); 
    xmm1 = _mm_load_si128(source_128++); 
    xmm2 = _mm_load_si128(source_128++); 
    xmm3 = _mm_load_si128(source_128++); 
    xmm4 = _mm_load_si128(source_128++); 
    xmm5 = _mm_load_si128(source_128++); 
    xmm6 = _mm_load_si128(source_128++); 
    xmm7 = _mm_load_si128(source_128++); 

    _mm_stream_si128(dest_128++, xmm0); 
    _mm_stream_si128(dest_128++, xmm1); 
    _mm_stream_si128(dest_128++, xmm2); 
    _mm_stream_si128(dest_128++, xmm3); 
    _mm_stream_si128(dest_128++, xmm4); 
    _mm_stream_si128(dest_128++, xmm5); 
    _mm_stream_si128(dest_128++, xmm6); 
    _mm_stream_si128(dest_128++, xmm7); 
} 
} 
+2

Hay alguna posibilidad de que podamos conseguir el código fuente "original", así, sólo para obtener una visión general de lo que su código está haciendo? – jalf

Respuesta

3

eax + 70h de lectura se mueve hacia arriba debido a eax + 70h se encuentra en una línea de caché diferente de EAX, y el compilador probablemente quiere que el precaptador de hardware para estar ocupado obteniendo esa línea lo antes posible.

No se entrelaza porque quiere maximizar el rendimiento al evitar dependencias de carga en la tienda (aunque la guía de optimización AMD explícitamente dice intercalar), o simplemente porque no está seguro de que las tiendas sobrescriban cargas . ¿Cambia el comportamiento si agrega __restrict palabras clave a source y dest?

El propósito del resto también me elude. Podría haber alguna descodificación oscura de la instrucción o consideraciones de captador de hardware, ya sea para AMD o Intel, pero no puedo encontrar ninguna justificación para eso. Me pregunto si el código se vuelve más rápido o más lento cuando eliminas esas instrucciones.

La distancia de precaptura recomendada depende del tamaño del bucle. Necesita estar lo suficientemente lejos como para que los datos tengan tiempo de llegar de la memoria cuando sea necesario. Creo que por lo general necesitas darle al menos 100 tics de reloj.

+0

__restrict hace que el montaje sea horrible. Solo usa un registro sse e incrementa los registros después de cada operación. – ronag

+1

@ronag: eso es interesante. No puedo imaginarme por qué 'restrict' resultaría en un código * slower *. Puede valer la pena enviarlo a MS Connect. – jalf

2

No he descubierto lo que hace el compilador, sin embargo, creo que compartiría algunos de mis resultados de prueba. He reescrito la función en ensamblaje.

sistema: Xeon W3520

4,55 Gb/s: memcpy regular de

5,52 GB/s: memcpy en cuestión

5,58 GB/s: memcpy debajo

7.48 GB/s: establecimiento de memoria por debajo de multiproceso

void* memcpy(void* dest, const void* source, size_t num) 
{ 
    __asm 
    { 
     mov esi, source;  
     mov edi, dest; 

     mov ebx, num; 
     shr ebx, 7;  

     cpy: 
      prefetchnta [esi+80h]; 
      prefetchnta [esi+0C0h]; 

      movdqa xmm0, [esi+00h]; 
      movdqa xmm1, [esi+10h]; 
      movdqa xmm2, [esi+20h]; 
      movdqa xmm3, [esi+30h]; 

      movntdq [edi+00h], xmm0; 
      movntdq [edi+10h], xmm1; 
      movntdq [edi+20h], xmm2; 
      movntdq [edi+30h], xmm3; 

      movdqa xmm4, [esi+40h]; 
      movdqa xmm5, [esi+50h]; 
      movdqa xmm6, [esi+60h]; 
      movdqa xmm7, [esi+70h]; 

      movntdq [edi+40h], xmm4; 
      movntdq [edi+50h], xmm5; 
      movntdq [edi+60h], xmm6; 
      movntdq [edi+70h], xmm7; 

      lea edi, [edi+80h]; 
      lea esi, [esi+80h]; 
      dec ebx; 

     jnz cpy; 
    } 
    return dest; 
} 

void* memcpy_tbb(void* dest, const void* source, size_t num) 
{ 
    tbb::parallel_for(tbb::blocked_range<size_t>(0, num/128), [&](const tbb::blocked_range<size_t>& r) 
    { 
     memcpy_SSE2_3(reinterpret_cast<char*>(dest) + r.begin()*128, reinterpret_cast<const char*>(source) + r.begin()*128, r.size()*128); 
    }, tbb::affinity_partitioner()); 

    return dest; 
} 
+0

Su memcpy usa instrucciones alineadas. Pero, ¿cómo podemos estar seguros de que la estructura de datos está alineada y que copiamos? – bluejamesbond

1
0052AC82 mov   edx,eax  // 
0052AC84 movdqa  xmm7,xmmword ptr [edx] // 3. Why edx? and not simply eax? <-- 

además probablemente porque quiere dividir el camino de datos por lo que esta instrucción

0052ACAF sub   eax,0FFFFFF80h 

puede ejecutar en paralelo.

Punto número 4 podría ser una pista para la captura previa ... además probablemente (ya que de lo contrario no tiene ningún sentido, también podría ser un compilador/optimizador de errores/capricho).

no tengo ninguna idea sobre el punto 5

Cuestiones relacionadas