2010-10-27 15 views
9

Kompiluję trochę kodu przy użyciu następujących ustawień w VC++ 2010:/O2/Ob2/Oi/OtCo robi mój kompilator? (optymalizacja memcpy)

Mam jednak pewne problemy ze zrozumieniem niektórych części generowanego zespołu , Zadałem kilka pytań w kodzie jako komentarze.

Ponadto, jaka odległość do wstępnej selekcji jest ogólnie zalecana w nowoczesnych procesorach? Mogę testować na moim własnym procesorze, ale liczyłem na jakąś wartość, która będzie dobrze działać w szerszym zakresie cpus. Może można użyć dynamicznych odległości do wstępnego pobierania?

< --edit:

Inną rzeczą Dziwię się o to, że kompilator nie przeplatają się w jakiejś formie instrukcje movdqa i movntdq? Ponieważ te instrukcje są w pewnym sensie asynchroniczne z mojego zrozumienia.

Ten kod zakłada również 32-bajtowe wiersze pamięci podręcznej podczas wstępnego pobierania, jednak wydaje się, że procesory wysokiej klasy mają 64-bajtowe cacheliny, więc prawdopodobnie dwa z nich mogą zostać usunięte.

->

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

oryginalny kod:

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

jakaś szansa możemy uzyskać „oryginał”, a także kod źródłowy, tak aby uzyskać przegląd tego, co robi Twój kod? – jalf

Odpowiedz

3

eax + 70h odczytu jest przesuwany w górę ze względu eax + 70h znajduje się w innej linii cache od eax, a kompilator prawdopodobnie chce sprzętowy prefetcher, aby jak najszybciej uzyskać dostęp do tej linii.

Nie przeplata się również dlatego, że chce zmaksymalizować wydajność, unikając zależności między ładowaniem a przechowywaniem (nawet jeśli przewodnik optymalizacji AMD wyraźnie mówi o przeplataniu) lub po prostu dlatego, że nie jest pewne, czy sklepy nie nadpiszeją obciążeń . Czy zmienia zachowanie, jeśli dodasz __restrict keywords do source i dest?

Cel reszty tego też mi umyka. Może to być jakieś niejasne rozszyfrowanie instrukcji lub rozważania dotyczące sprzętu, zarówno dla AMD, jak i Intela, ale nie mogę znaleźć żadnego uzasadnienia. Zastanawiam się, czy kod usuwa się szybciej lub wolniej, gdy usuniesz te instrukcje?

Zalecana odległość pobierania wstępnego zależy od wielkości pętli. Musi być wystarczająco daleko, aby dane miały czas dotrzeć z pamięci do czasu, kiedy jest to potrzebne. Myślę, że zwykle musisz dać mu co najmniej 100 tyknięć zegara.

+0

__restrict sprawia, że ​​montaż jest okropny. Używa tylko jednego rejestru sse i rejestrów przyrostów po każdej operacji. – ronag

+1

@ronag: To interesujące. Nie mogę sobie wyobrazić, dlaczego 'restrict' kiedykolwiek spowodowałby * wolniejszy * kod. Może warto przesłać do MS Connect. – jalf

2

Nie mam pojęcia, co robi kompilator, ale mimo to udostępniam niektóre wyniki testów. Przepisałem funkcję w zespole.

System: Xeon W3520

4,55 GB/s: regularne memcpy

5,52 GB/s: memcpy w pytaniu

5.58 GB/s: memcpy poniżej

7.48 GB/s: memcpy poniżej wielowątkowym

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

Twój memcpy używa wyrównanych instrukcji. Ale w jaki sposób możemy mieć pewność, że struktura danych jest zgodna, że ​​kopiujemy? – bluejamesbond

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

ponieważ chce propably podzielić DataPath więc ta instrukcja

0052ACAF sub   eax,0FFFFFF80h 

mogą być wykonywane równolegle.

Numer punktu może być wskazówką dla prefetchera ... prawdopodobnie (ponieważ w przeciwnym razie nie ma sensu, może być również błędem kompilatora/optymalizatora/dziwactwa).

nie mam żadnego pojęcia o punkcie