9 Stimmen

Was macht mein Compiler? (memcpy optimieren)

Ich kompiliere ein bisschen Code mit den folgenden Einstellungen in VC++2010: /O2 /Ob2 /Oi /Ot

Ich habe jedoch Schwierigkeiten, einige Teile des generierten Assembly-Codes zu verstehen, ich habe einige Fragen im Code als Kommentare hinterlegt.

Außerdem, welche Prefetching-Distanz wird im Allgemeinen auf modernen CPUs empfohlen? Ich kann es natürlich auf meiner eigenen CPU testen, aber ich hoffte auf einen Wert, der auf einer breiteren Palette von CPUs gut funktioniert. Vielleicht könnte man dynamische Prefetching-Distanzen verwenden?

<--EDIT:

Etwas, über das ich überrascht bin, ist, dass der Compiler die movdqa- und movntdq-Anweisungen nicht in irgendeiner Form überkreuzt? Da diese Anweisungen meiner Meinung nach in gewisser Weise asynchron sind.

Dieser Code geht auch von 32-Byte-Cachelines beim Prefetching aus, jedoch scheint es, dass High-End-CPUs 64-Byte-Cachelines haben, sodass 2 der Prefetches wahrscheinlich entfernt werden können.

-->

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(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(source_128+8), _MM_HINT_NTA);
  _mm_prefetch(reinterpret_cast(source_128+10), _MM_HINT_NTA);
  _mm_prefetch(reinterpret_cast(source_128+12), _MM_HINT_NTA);
  _mm_prefetch(reinterpret_cast(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. Warum wird dies vor den Prefetches verschoben?
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. Was passiert in diesen 2 Zeilen?
0052AC82  mov         edx,eax        //
0052AC84  movdqa      xmm7,xmmword ptr [edx]  // 3. Warum edx? und nicht einfach eax?

  _mm_stream_si128(dest_128++, xmm0);
0052AC88  mov         esi,ecx  // 4. Wird esi nie verwendet?
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. Warum nicht einfach ecx?
0052ACBA  jne         copy+20h (52AC40h)  
0052ACBC  pop         edi  
0052ACBD  pop         esi  
 }
}

Ursprünglicher Code:

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(source);

 for(size_t n = 0; n < size/16; n += 8) 
 {
  _mm_prefetch(reinterpret_cast(source_128+8), _MM_HINT_NTA);
  _mm_prefetch(reinterpret_cast(source_128+10), _MM_HINT_NTA);
  _mm_prefetch(reinterpret_cast(source_128+12), _MM_HINT_NTA);
  _mm_prefetch(reinterpret_cast(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);
 }
}

3voto

Eugene Smith Punkte 8838

Die eax+70h-Leseoperation wird nach oben verschoben, weil eax+70h in einer anderen Cache-Zeile als eax liegt und der Compiler wahrscheinlich möchte, dass der Hardware-Prefetcher so bald wie möglich diese Zeile holt.

Es wird auch nicht verzweigt, weil es die Leistung maximieren möchte, indem es Lade-zu-Speicher-Abhängigkeiten vermeidet (obwohl der AMD-Optimierungsleitfaden explizit sagt, dass verzweigt werden soll), oder einfach weil es nicht sicher ist, dass Speicheroperationen Leseoperationen überschreiben. Ändert sich das Verhalten, wenn Sie die __restrict-Schlüsselwörter zu Quelle und Ziel hinzufügen?

Der Rest davon ist mir auch nicht klar. Könnte sich um eine obskure Anweisungsdecodierung oder Hardware-Prefetcher-Überlegungen handeln, entweder für AMD oder Intel, aber ich finde keine Rechtfertigung dafür. Ich frage mich, ob der Code schneller oder langsamer wird, wenn Sie diese Anweisungen entfernen?

Der empfohlene Prefetch-Abstand hängt von der Schleifengröße ab. Er muss weit genug sein, damit die Daten rechtzeitig aus dem Speicher ankommen, wenn sie benötigt werden. Ich denke, dass Sie ihm in der Regel mindestens 100 Taktzyklen geben müssen.

2voto

ronag Punkte 46121

Ich habe noch nicht herausgefunden, was der Compiler macht, aber ich dachte, ich teile einige meiner Testergebnisse. Ich habe die Funktion in Assembler neu geschrieben.

System: Xeon W3520

4,55 GB/s : reguläres memcpy

5,52 GB/s : betroffenes memcpy

5,58 GB/s : darunterliegendes memcpy

7,48 GB/s : darunterliegendes mehrfädiges memcpy

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(0, num/128), [&](const tbb::blocked_range& r)
    {
        memcpy_SSE2_3(reinterpret_cast(dest) + r.begin()*128, reinterpret_cast(source) + r.begin()*128, r.size()*128);
    }, tbb::affinity_partitioner());

    return dest;
}

1voto

Quonux Punkte 2869
0052AC82  mov         edx,eax        //
0052AC84  movdqa      xmm7,xmmword ptr [edx]  // 3. Warum edx? und nicht einfach eax? <--

weil es wahrscheinlich den Datenpfad teilen will, damit diese Anweisung

0052ACAF  sub         eax,0FFFFFF80h  

parallel ausgeführt werden kann.

Punkt Nummer 4 könnte ein Hinweis für den Prefetcher sein...wahrscheinlich (weil sonst ergibt es keinen Sinn, könnte auch ein Compiler/Optimierer Fehler/ Eigenheit sein).

Ich habe keine Ahnung von Punkt 5

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X