2010-10-27 10 views
9

Je compile un peu de code en utilisant les paramètres suivants dans VC++ 2010:/O2/Ob2/Oi/OtQue fait mon compilateur? (Optimisation memcpy)

Cependant, je vais avoir du mal à comprendre certaines parties de l'ensemble généré , J'ai mis quelques questions dans le code comme commentaires.

En outre, quelle distance de préextraction est généralement recommandée sur les processeurs modernes? Je peux tester sur mon propre processeur, mais j'espérais une valeur qui fonctionnera bien sur un plus large éventail de processeurs. Peut-être que l'on pourrait utiliser des distances de prélecture dynamique?

< --edit:

Une autre chose que je suis surpris que le compilateur n'entrelacer pas sous une forme les instructions de movdqa et movntdq? Puisque ces instructions sont en quelque sorte asynchrones de ma compréhension.

Ce code suppose également des lignes de cache de 32 octets lors de la pré-extraction, mais il semble que les processeurs haut de gamme ont des lignes de 64 octets, de sorte que 2 des préfixes peuvent probablement être supprimés.

->

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 
} 
} 

Code d'origine:

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

Toute chance que nous pouvons obtenir le code source « d'origine » et, juste pour obtenir un aperçu de ce que votre code est en train de faire? – jalf

Répondre

3

eax + lecture de 70h est déplacé parce que eax + 70h est dans une autre ligne de cache de eax, et le compilateur veut probablement le pré-sélectionneur de matériel pour être occupé à obtenir cette ligne dès que possible. Il n'entrelace pas non plus parce qu'il veut optimiser les performances en évitant les dépendances charge-à-stocker (même si le guide d'optimisation AMD dit explicitement à entrelacer), ou simplement parce qu'il n'est pas sûr que les magasins n'écrasent pas les charges . Change-t-il le comportement si vous ajoutez des mots-clés __restrict à la source et à la dest?

Le but du reste me échappe aussi. Pourrait être quelques considérations obscures de décodage d'instruction ou de pré-sélectionneur de matériel, pour AMD ou Intel, mais je ne peux trouver aucune justification pour cela. Je me demande si le code devient plus rapide ou plus lent lorsque vous supprimez ces instructions?

La distance de pré-chargement recommandée dépend de la taille de la boucle. Doit être assez loin que les données ont le temps d'arriver de la mémoire au moment où il est nécessaire. Je pense que vous devez habituellement lui donner au moins 100 ticks d'horloge.

+0

__restrict rend l'assemblage horrible. Il utilise seulement un registre sse et incrémente les registres après chaque opération. – ronag

+1

@ronag: c'est intéressant. Je ne peux pas imaginer pourquoi 'restrict' aboutirait jamais à un code plus lent. Cela pourrait valoir la peine d'être soumis à MS Connect. – jalf

2

Je n'ai pas compris ce que fait le compilateur, cependant je partagerais certains de mes résultats de test. J'ai réécrit la fonction dans l'assemblage.

Système: Xeon W3520

4,55 GB/s: memcpy régulière

5,52 GB/s: memcpy en question

5,58 GB/s: memcpy ci-dessous

7.48 Go/s: memcpy ci-dessous multithread

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

Votre memcpy utilise des instructions alignées. Mais, comment pouvons-nous être sûrs que la structure de données est alignée que nous copions? – bluejamesbond

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

parce qu'il veut propably de diviser le datapath si cette instruction

0052ACAF sub   eax,0FFFFFF80h 

peut être exécuté en parallèle.

Le numéro de point peut être un indice pour le prefetcher ... propably (car sinon cela n'a aucun sens, pourrait également être un compilateur/optimiseur bug/quirk).

Je n'ai aucune idée sur le point

Questions connexes