2011-10-27 41 views
13

Bài đăng này liên quan chặt chẽ đến một bài đăng khác mà tôi đã đăng some days ago. Lần này, tôi đã viết một mã đơn giản chỉ cần thêm một mảng các phần tử, nhân kết quả bằng các giá trị trong mảng khác và lưu nó trong một mảng thứ ra, tất cả các biến floating point double precision typed.Tối ưu hóa mã SSE của GCC

Tôi đã tạo hai phiên bản của mã đó: một phiên bản có hướng dẫn SSE, sử dụng cuộc gọi đến và một lệnh khác mà không có chúng, sau đó chúng được biên dịch bằng mức tối ưu hóa gcc và -O0. Tôi viết chúng dưới đây:

// SSE VERSION 

#define N 10000 
#define NTIMES 100000 
#include <time.h> 
#include <stdio.h> 
#include <xmmintrin.h> 
#include <pmmintrin.h> 

double a[N] __attribute__((aligned(16))); 
double b[N] __attribute__((aligned(16))); 
double c[N] __attribute__((aligned(16))); 
double r[N] __attribute__((aligned(16))); 

int main(void){ 
    int i, times; 
    for(times = 0; times < NTIMES; times++){ 
    for(i = 0; i <N; i+= 2){ 
     __m128d mm_a = _mm_load_pd(&a[i]); 
     _mm_prefetch(&a[i+4], _MM_HINT_T0); 
     __m128d mm_b = _mm_load_pd(&b[i]); 
     _mm_prefetch(&b[i+4] , _MM_HINT_T0); 
     __m128d mm_c = _mm_load_pd(&c[i]); 
     _mm_prefetch(&c[i+4] , _MM_HINT_T0); 
     __m128d mm_r; 
     mm_r = _mm_add_pd(mm_a, mm_b); 
     mm_a = _mm_mul_pd(mm_r , mm_c); 
     _mm_store_pd(&r[i], mm_a); 
     } 
    } 
} 

//NO SSE VERSION 
//same definitions as before 
int main(void){ 
    int i, times; 
    for(times = 0; times < NTIMES; times++){ 
    for(i = 0; i < N; i++){ 
     r[i] = (a[i]+b[i])*c[i]; 
    } 
    } 
} 

Khi biên dịch chúng với -O0, gcc tận dụng XMM/ghi MMX và SSE intstructions, nếu không được đưa ra cụ thể là tùy chọn -mno-SSE (và những người khác). Tôi đã kiểm tra mã lắp ráp được tạo cho mã thứ hai và tôi nhận thấy rằng mã này sử dụng các mã số movsd, addsd mulsd. Vì vậy, nó làm cho việc sử dụng các lệnh SSE nhưng chỉ có những người sử dụng phần thấp nhất của thanh ghi, nếu tôi không sai. Mã lắp ráp được tạo cho mã C đầu tiên được sử dụng, như mong đợi, của các hướng dẫn thêm addp mulpd, mặc dù mã lắp ráp lớn hơn đã được tạo.

Dù sao, mã đầu tiên sẽ nhận được lợi nhuận tốt hơn, theo như tôi biết, của mô hình SIMD, vì mỗi lần lặp lại hai giá trị kết quả được tính toán. Tuy nhiên, mã thứ hai thực hiện một cái gì đó nhanh hơn 25 phần trăm so với cái đầu tiên. Tôi cũng đã thực hiện một thử nghiệm với các giá trị độ chính xác đơn và nhận được kết quả tương tự. Lý do cho điều đó là gì?

+5

So sánh hiệu suất khi biên dịch mà không tối ưu hóa là vô nghĩa. – interjay

+1

Bạn đang làm 3 x tải và 1 x lưu trữ chỉ với 2 phép tính số học x, vì vậy bạn rất có thể sẽ bị giới hạn băng thông. –

+5

Điều gì sẽ xảy ra khi bạn xóa lệnh gọi _mm_prefetch? Tôi nghĩ rằng họ có thể làm tổn thương bạn – TJD

Trả lời

14

Vectorization trong GCC được bật tại -O3. Đó là lý do tại -O0, bạn chỉ thấy các hướng dẫn SSE2 vô hướng thông thường (movsd, addsd, v.v ...). Sử dụng GCC 4.6.1 và ví dụ thứ hai của bạn:

#define N 10000 
#define NTIMES 100000 

double a[N] __attribute__ ((aligned (16))); 
double b[N] __attribute__ ((aligned (16))); 
double c[N] __attribute__ ((aligned (16))); 
double r[N] __attribute__ ((aligned (16))); 

int 
main (void) 
{ 
    int i, times; 
    for (times = 0; times < NTIMES; times++) 
    { 
     for (i = 0; i < N; ++i) 
     r[i] = (a[i] + b[i]) * c[i]; 
    } 

    return 0; 
} 

và biên soạn với gcc -S -O3 -msse2 sse.c sản xuất cho vòng lặp bên trong các hướng dẫn sau đây, mà là khá tốt:

.L3: 
    movapd a(%eax), %xmm0 
    addpd b(%eax), %xmm0 
    mulpd c(%eax), %xmm0 
    movapd %xmm0, r(%eax) 
    addl $16, %eax 
    cmpl $80000, %eax 
    jne .L3 

Như bạn có thể thấy, với vector cho phép GCC phát ra mã để thực hiện hai lần lặp song song lặp lại. Nó có thể được cải thiện, mặc dù - mã này sử dụng 128 bit thấp hơn của thanh ghi SSE, nhưng nó có thể sử dụng đầy đủ thanh ghi YMM 256 bit, bằng cách bật mã hóa AVX của lệnh SSE (nếu có trên máy). Vì vậy, biên soạn chương trình tương tự với gcc -S -O3 -msse2 -mavx sse.c cho cho vòng lặp bên trong:

.L3: 
    vmovapd a(%eax), %ymm0 
    vaddpd b(%eax), %ymm0, %ymm0 
    vmulpd c(%eax), %ymm0, %ymm0 
    vmovapd %ymm0, r(%eax) 
    addl $32, %eax 
    cmpl $80000, %eax 
    jne .L3 

Lưu ý rằng v trước mỗi giảng dạy và hướng dẫn sử dụng 256-bit YMM đăng ký, Bốn lặp của vòng lặp ban đầu được thực hiện song song.

+0

Tôi chỉ chạy điều này thông qua 'gcc 4.7.2' trên' x86-64' có và không có cờ '-msse2' - cả hai kết quả trong cùng một đầu ra của bộ kết hợp. Vì vậy, nó sẽ được an toàn để sse hướng dẫn được kích hoạt theo mặc định trên nền tảng này? –

+0

@lori, vâng, SSE là mặc định trên x86-64. – chill

+0

Ở đây bạn có thể kiểm tra với các trình biên dịch khác nhau http://goo.gl/bM62CZ – KindDragon

2

Tôi muốn mở rộng chill's answer và thu hút sự chú ý của bạn về thực tế là GCC dường như không thể sử dụng cùng một hướng dẫn thông minh của hướng dẫn AVX khi lặp lại ngược.

Chỉ cần thay thế vòng lặp bên trong mã mẫu lạnh với:

for (i = N-1; i >= 0; --i) 
    r[i] = (a[i] + b[i]) * c[i]; 

GCC (4.8.4) với các tùy chọn -S -O3 -mavx sản xuất:

.L5: 
    vmovsd a+79992(%rax), %xmm0 
    subq $8, %rax 
    vaddsd b+80000(%rax), %xmm0, %xmm0 
    vmulsd c+80000(%rax), %xmm0, %xmm0 
    vmovsd %xmm0, r+80000(%rax) 
    cmpq $-80000, %rax 
    jne  .L5 
+0

Thú vị. Mới hơn gcc tự động vectơ vui nhộn, sử dụng 'vpermpd 0b00011011' cho mỗi đầu vào/đầu ra của mảng để đảo ngược nó sau khi tải, do đó các phần tử dữ liệu trong mỗi vectơ đi từ đầu đến cuối theo thứ tự nguồn. Đó là 4 'vpermpd's mỗi lần lặp! Điều thú vị là, [clang tự động-vectorizes nó độc đáo] (https://godbolt.org/g/azbIIi) –

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