2009-12-27 14 views
40

Có ai nhìn thấy bất kỳ số liệu/phân tích nào về việc sử dụng từ khóa C/C++ restrict trong gcc/g ++ thực tế hay không. học thuyết)?Từ khóa giới hạn có mang lại lợi ích đáng kể trong gcc/g ++

Tôi đã đọc các bài viết khác nhau đề xuất/disparaging nó sử dụng, nhưng tôi đã không chạy trên bất kỳ số thực tế thực tế chứng minh hai bên đối số.

EDIT

Tôi biết rằng restrict không phải là chính thức một phần của C++, nhưng nó được hỗ trợ bởi một số trình biên dịch và tôi đã đọc một bài báo bằng Christer Ericson mà mạnh mẽ khuyến cáo đó là cách sử dụng.

+9

vấn đề Aliasing là thường được coi là lý do số 1 tại sao C/C++ kém hiệu quả hơn trong nhiều nhiệm vụ tính toán hơn Fortran. Vì vậy, tôi muốn nói bất kỳ tính năng nào giúp tránh việc tạo bí danh có thể tạo ra sự khác biệt * lớn *. – jalf

+0

có thể trùng lặp với [Sử dụng thực tế từ khóa 'giới hạn' của C99?] (Http://stackoverflow.com/questions/745870/realistic-usage-of-the-c99-restrict-keyword) –

Trả lời

41

Từ khóa hạn chế có sự khác biệt.

Tôi đã thấy các cải thiện của yếu tố 2 trở lên trong một số trường hợp (xử lý hình ảnh). Hầu hết thời gian sự khác biệt không phải là lớn mặc dù. Khoảng 10%.

Dưới đây là một ví dụ nhỏ minh họa sự khác biệt. Tôi đã viết một phép biến đổi ma trận 4x4 vector rất cơ bản như một bài kiểm tra. Lưu ý rằng tôi phải buộc hàm không được gạch chân. Nếu không, GCC phát hiện rằng không có bất kỳ con trỏ bí danh nào trong mã điểm chuẩn của tôi và các giới hạn sẽ không tạo ra sự khác biệt do nội tuyến.

Tôi cũng có thể đã chuyển hàm chuyển đổi sang một tệp khác.

#include <math.h> 

#ifdef USE_RESTRICT 
#else 
#define __restrict 
#endif 


void transform (float * __restrict dest, float * __restrict src, 
       float * __restrict matrix, int n) __attribute__ ((noinline)); 

void transform (float * __restrict dest, float * __restrict src, 
       float * __restrict matrix, int n) 
{ 
    int i; 

    // simple transform loop. 

    // written with aliasing in mind. dest, src and matrix 
    // are potentially aliasing, so the compiler is forced to reload 
    // the values of matrix and src for each iteration. 

    for (i=0; i<n; i++) 
    { 
    dest[0] = src[0] * matrix[0] + src[1] * matrix[1] + 
       src[2] * matrix[2] + src[3] * matrix[3]; 

    dest[1] = src[0] * matrix[4] + src[1] * matrix[5] + 
       src[2] * matrix[6] + src[3] * matrix[7]; 

    dest[2] = src[0] * matrix[8] + src[1] * matrix[9] + 
       src[2] * matrix[10] + src[3] * matrix[11]; 

    dest[3] = src[0] * matrix[12] + src[1] * matrix[13] + 
       src[2] * matrix[14] + src[3] * matrix[15]; 

    src += 4; 
    dest += 4; 
    } 
} 

float srcdata[4*10000]; 
float dstdata[4*10000]; 

int main (int argc, char**args) 
{ 
    int i,j; 
    float matrix[16]; 

    // init all source-data, so we don't get NANs 
    for (i=0; i<16; i++) matrix[i] = 1; 
    for (i=0; i<4*10000; i++) srcdata[i] = i; 

    // do a bunch of tests for benchmarking. 
    for (j=0; j<10000; j++) 
    transform (dstdata, srcdata, matrix, 10000); 
} 

Kết quả: (trên 2 Ghz tôi Core Duo)

[email protected]:~$ gcc -O3 test.c 
[email protected]:~$ time ./a.out 

real 0m2.517s 
user 0m2.516s 
sys  0m0.004s 

[email protected]:~$ gcc -O3 -DUSE_RESTRICT test.c 
[email protected]:~$ time ./a.out 

real 0m2.034s 
user 0m2.028s 
sys  0m0.000s 

Trong ngón tay cái nhanh hơn 20% thực hiện, trên rằng hệ thống.

Để hiển thị số lượng tùy thuộc vào kiến ​​trúc tôi đã cho phép mã giống nhau chạy trên CPU được nhúng Cortex-A8 (điều chỉnh số vòng lặp một chút khiến tôi không muốn đợi lâu):

[email protected]:~# gcc -O3 -mcpu=cortex-a8 -mfpu=neon -mfloat-abi=softfp test.c 
[email protected]:~# time ./a.out 

real 0m 7.64s 
user 0m 7.62s 
sys  0m 0.00s 

[email protected]:~# gcc -O3 -mcpu=cortex-a8 -mfpu=neon -mfloat-abi=softfp -DUSE_RESTRICT test.c 
[email protected]:~# time ./a.out 

real 0m 7.00s 
user 0m 6.98s 
sys  0m 0.00s 

đây là sự khác biệt chỉ là 9% (cùng biên dịch btw.)

+2

Công việc tuyệt vời. Có một bài viết về việc sử dụng các hạn chế đối với bộ xử lý Cell tại đây: http://cellperformance.beyond3d.com/articles/2006/05/demystifying-the-restrict-keyword.html có thể liên quan đến lợi ích cụ thể của kiến ​​trúc thảo luận . – Clifford

+0

@Nils Pipenbrinck: Tại sao bạn phải tắt nội tuyến cho chức năng? Nó có vẻ giống như một chức năng rất lớn cho trình biên dịch để tự động nội tuyến. –

+2

@Nils Pipenbrinck: Bằng cách Ulrich Drepper đăng mã cho một ma trận siêu tối đa nhân như một phần của cuộc thảo luận của ông về tối ưu hóa bộ nhớ cache và sử dụng bộ nhớ. Nó ở đây: http://lwn.net/Articles/258188/. Thảo luận của ông về từng bước ông đã đi qua để đi đến giải pháp đó là ở đây: http://lwn.net/Articles/255364/. Anh ta có thể giảm thời gian thực hiện xuống 90% so với một MM chuẩn. –

0

Tôi đã thử nghiệm this Chương trình C. Nếu không có restrict mất 12.640 giây để hoàn thành, với restrict 12.516. Có vẻ như số điện thoại có thể tiết kiệm một số thời gian.

+23

Sự khác biệt đó là tiếng ồn đo lường ... –

+0

Sự khác biệt đó gần như chắc chắn không đáng kể, tuy nhiên, bạn cũng nên khai báo c là bị hạn chế vì mỗi lần c được ghi vào thời điểm trình biên dịch có thể xem xét * a * b và * inc có thể đã thay đổi. – James

+0

Trong ví dụ của bạn, trình tối ưu hóa có thể phát hiện các thông số không có răng cưa. Cố gắng vô hiệu hóa nội tuyến và bạn sẽ thấy sự khác biệt lớn hơn. –

0

Lưu ý rằng trình biên dịch C++ cho phép từ khóa restrict vẫn có thể bỏ qua nó. Đó là trường hợp ví dụ: here.

+0

Trên thực tế, nếu bạn đọc xuống trang bạn sẽ nhận thấy rằng trong khi giới hạn bị bỏ qua trong C++ vì xung đột tiềm năng với biến người dùng có cùng tên, '__restrict__' được hỗ trợ cho C++. –

+1

@Robert: Và bị bỏ qua. Sự khác biệt chỉ là các số nhận dạng có dấu gạch dưới kép được dành riêng cho việc sử dụng hệ thống. Vì vậy, một \ _ \ _ hạn chế \ _ \ _ không nên xung đột với bất kỳ người dùng nào được khai báo định danh. –

+0

@Martin: Làm sao bạn biết nó bị bỏ qua? Nó không hoàn toàn rõ ràng từ các tài liệu - có vẻ như bạn có thể đọc nó một trong hai cách. –

6

bài viết Demystifying The Restrict Keyword đề cập đến giấy Why Programmer-specified Aliasing is a Bad Idea (pdf) mà nói nó thường không giúp đỡ và cung cấp các phép đo để trở lại này lên.

+0

Có rất nhiều loại mã mà nó cung cấp ít lợi ích, nhưng có một số nơi nó cung cấp một lợi ích rất lớn. Bạn có biết bất kỳ giấy tờ nào cho thấy rằng việc sử dụng "hạn chế" một cách khôn ngoan sẽ không mang lại lợi ích lớn hơn những trình biên dịch có thể nhận ra thông qua việc đánh răng dựa trên loại không? – supercat

3

Từ khóa giới hạn có mang lại lợi ích đáng kể trong gcc/g ++ không?

có thể giảm số lượng hướng dẫn như minh họa trong ví dụ bên dưới, vì vậy hãy sử dụng nó bất cứ khi nào có thể.

GCC 4.8 Linux x86-64 exmample

Input:

void f(int *a, int *b, int *x) { 
    *a += *x; 
    *b += *x; 
} 

void fr(int *restrict a, int *restrict b, int *restrict x) { 
    *a += *x; 
    *b += *x; 
} 

Compile và biên soạn lại:

gcc -g -std=c99 -O0 -c main.c 
objdump -S main.o 

Với -O0, họ đều giống nhau.

Với -O3:

void f(int *a, int *b, int *x) { 
    *a += *x; 
    0: 8b 02     mov (%rdx),%eax 
    2: 01 07     add %eax,(%rdi) 
    *b += *x; 
    4: 8b 02     mov (%rdx),%eax 
    6: 01 06     add %eax,(%rsi) 

void fr(int *restrict a, int *restrict b, int *restrict x) { 
    *a += *x; 
    10: 8b 02     mov (%rdx),%eax 
    12: 01 07     add %eax,(%rdi) 
    *b += *x; 
    14: 01 06     add %eax,(%rsi) 

Đối với người lãnh đạo, các calling convention là:

  • rdi = tham số đầu tiên
  • rsi = tham số thứ hai
  • rdx = tham số thứ ba

Kết luận: 3 hướng dẫn thay vì 4.

Tất nhiên, hướng dẫn can have different latencies, nhưng điều này mang lại ý tưởng hay.

Tại sao GCC có thể tối ưu hóa điều đó?

Mã trên được lấy từ Wikipedia examplerất chiếu sáng.

Pseudo lắp ráp cho f:

load R1 ← *x ; Load the value of x pointer 
load R2 ← *a ; Load the value of a pointer 
add R2 += R1 ; Perform Addition 
set R2 → *a  ; Update the value of a pointer 
; Similarly for b, note that x is loaded twice, 
; because a may be equal to x. 
load R1 ← *x 
load R2 ← *b 
add R2 += R1 
set R2 → *b 

Đối fr:

load R1 ← *x 
load R2 ← *a 
add R2 += R1 
set R2 → *a 
; Note that x is not reloaded, 
; because the compiler knows it is unchanged 
; load R1 ← *x 
load R2 ← *b 
add R2 += R1 
set R2 → *b 

Là nó thực sự nào nhanh hơn?

Ermmm ... không cho kiểm tra đơn giản này:

.text 
    .global _start 
    _start: 
     mov $0x10000000, %rbx 
     mov $x, %rdx 
     mov $x, %rdi 
     mov $x, %rsi 
    loop: 
     # START of interesting block 
     mov (%rdx),%eax 
     add %eax,(%rdi) 
     mov (%rdx),%eax # Comment out this line. 
     add %eax,(%rsi) 
     # END ------------------------ 
     dec %rbx 
     cmp $0, %rbx 
     jnz loop 
     mov $60, %rax 
     mov $0, %rdi 
     syscall 
.data 
    x: 
     .int 0 

Và sau đó:

as -o a.o a.S && ld a.o && time ./a.out 

trên Ubuntu 14.04 AMD64 CPU Intel i5-3210M.

Tôi thú nhận rằng tôi vẫn không hiểu các CPU hiện đại.Hãy cho tôi biết nếu quý vị:

  • tìm thấy một lỗ hổng trong phương pháp của tôi
  • tìm thấy một trường hợp thử nghiệm lắp ráp, nơi nó trở nên nhanh hơn nhiều
  • hiểu tại sao không có một sự khác biệt
Các vấn đề liên quan