2010-10-27 36 views
9

Tôi đang biên dịch một chút mã bằng cách sử dụng các cài đặt sau trong VC++ 2010:/O2/Ob2/Oi/OtTrình biên dịch của tôi đang làm gì? (tối ưu hóa memcpy)

Tuy nhiên tôi đang gặp một số vấn đề khi hiểu một số phần của hội đồng được tạo , Tôi đã đặt một số câu hỏi trong mã làm nhận xét.

Ngoài ra, khoảng cách tìm nạp trước thường được đề xuất trên cpus hiện đại là gì? Tôi có thể ofc thử nghiệm trên cpu của riêng tôi, nhưng tôi đã hy vọng cho một số giá trị mà sẽ làm việc tốt trên một phạm vi rộng hơn của cpus. Có lẽ một người có thể sử dụng khoảng cách tìm nạp trước động?

< --edit:

Một điều tôi ngạc nhiên về là trình biên dịch không interleave trong một số hình thức hướng dẫn movdqa và movntdq? Vì những hướng dẫn này có ý nghĩa không đồng bộ từ sự hiểu biết của tôi.

Mã này cũng giả định 32 byte dòng bộ nhớ cache khi tìm nạp trước, tuy nhiên có vẻ như CPU ​​cao cấp có bộ nhớ cache 64 byte, do đó, 2 trong số các tìm nạp trước có thể bị xóa.

->

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

mã ban đầu:

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

Bất kỳ cơ hội chúng ta có thể lấy mã nguồn "gốc" là tốt, chỉ để có được một cái nhìn tổng quan về những gì mã của bạn đang làm gì? – jalf

Trả lời

3

eax + 70h đọc được di chuyển lên vì eax + 70h là trong một dòng bộ nhớ cache khác nhau từ eax, và trình biên dịch có thể muốn các phần cứng prefetcher để có được bận rộn nhận được rằng dòng càng sớm càng tốt.

Nó không xen kẽ bởi vì nó muốn tối đa hóa hiệu suất bằng cách tránh phụ thuộc tải vào cửa hàng (mặc dù hướng dẫn tối ưu hóa AMD nói rõ ràng để xen kẽ) hoặc đơn giản là vì không chắc chắn các cửa hàng sẽ không ghi đè tải . Nó có thay đổi hành vi nếu bạn thêm __restrict từ khóa vào nguồn và đích không?

Mục đích của phần còn lại của nó cũng giúp tôi. Có thể là một số giải mã hướng dẫn tối nghĩa hoặc cân nhắc trước khi tải phần cứng, hoặc cho AMD hoặc Intel, nhưng tôi không thể tìm thấy bất kỳ lý do nào cho điều đó. Tôi tự hỏi liệu mã có nhanh hơn hoặc chậm hơn khi bạn xóa các hướng dẫn đó không?

Khoảng cách tìm nạp trước được đề xuất tùy thuộc vào kích thước vòng lặp. Cần đủ xa để dữ liệu có thời gian đến từ bộ nhớ trước khi cần. Tôi nghĩ rằng bạn thường cần phải cung cấp cho nó ít nhất 100 đồng hồ ve.

+0

__restrict làm cho việc lắp ráp trở nên khủng khiếp. Nó chỉ sử dụng một thanh ghi sse và các thanh ghi gia tăng sau mỗi thao tác. – ronag

+1

@ronag: thật thú vị. Tôi không thể tưởng tượng tại sao 'giới hạn' sẽ bao giờ dẫn đến * mã * chậm hơn. Có thể đáng để nộp cho MS Connect. – jalf

2

Tôi chưa tìm ra trình biên dịch làm gì, tuy nhiên tôi cho rằng tôi sẽ chia sẻ một số kết quả thử nghiệm của mình. Tôi đã viết lại hàm trong assembly.

hệ thống: Xeon W3520

4,55 GB/s: memcpy thường xuyên

5,52 GB/s: memcpy trong câu hỏi

5,58 GB/s: memcpy dưới

7.48 GB/s: memcpy dưới multithreaded

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

memcpy của bạn sử dụng hướng dẫn liên kết. Nhưng, làm thế nào chúng ta có thể chắc chắn rằng cấu trúc dữ liệu được căn chỉnh mà chúng ta sao chép? – bluejamesbond

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

vì nó muốn propably để phân chia các đường dữ liệu để hướng dẫn này

0052ACAF sub   eax,0FFFFFF80h 

thể được thực hiện song song.

Số điểm có thể là gợi ý cho trình tìm nạp trước ... có thể (vì không có ý nghĩa gì, cũng có thể là trình biên dịch/lỗi tối ưu/quirk).

tôi không có bất kỳ ý tưởng về điểm

Các vấn đề liên quan