Câu hỏi Từ khóa hạn chế có mang lại lợi ích đáng kể trong gcc / g ++ không


Có ai nhìn thấy bất kỳ con số / phân tích về việc có hay không sử dụng C / C ++ restrict từ khóa trong gcc / g ++ thực tế cung cấp bất kỳ tăng hiệu suất đáng kể trong thực tế (và không chỉ trong lý thuyết)?

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ố.

CHỈNH SỬA

tôi biết điều đó restrict không phải là một phần chính thức 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 cách Christer Ericson mà mạnh mẽ khuyến cáo sử dụng nó.


41
2017-12-27 08:04


gốc


Các vấn đề về bí danh 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 so với 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 to Sự khác biệt. - jalf
có thể trùng lặp Việc sử dụng thực tế từ khóa 'giới hạn' của C99? - Ciro Santilli 新疆改造中心 六四事件 法轮功


Các câu trả lời:


Các từ khóa hạn chế không một sự khác biệt.

Tôi đã thấy những cải tiến của yếu tố 2 và nhiều hơn nữa 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ể di 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 Core Duo của tôi)

nils@doofnase:~$ gcc -O3 test.c
nils@doofnase:~$ time ./a.out

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

nils@doofnase:~$ gcc -O3 -DUSE_RESTRICT test.c
nils@doofnase:~$ time ./a.out

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

Thực hiện nhanh hơn trên ngón tay cái 20% cái đó hệ thống.

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

root@beagleboard:~# gcc -O3 -mcpu=cortex-a8 -mfpu=neon -mfloat-abi=softfp test.c
root@beagleboard:~# time ./a.out

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

root@beagleboard:~# gcc -O3 -mcpu=cortex-a8 -mfpu=neon -mfloat-abi=softfp -DUSE_RESTRICT test.c 
root@beagleboard:~# time ./a.out

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

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


42
2017-12-27 18:31



Công việc tốt đẹp. Có một bài viết về việc sử dụng các giới hạn trên một bộ xử lý Cell ở đây: cellperformance.beyond3d.com/articles/2006/05/… có thể liên quan đến lợi ích cụ thể của kiến ​​trúc thảo luận. - Clifford
@Nils Pipenbrinck: Tại sao bạn phải vô hiệu hóa 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. - Robert S. Barnes
@Nils Pipenbrinck: Bằng cách Ulrich Drepper đăng mã cho một ma trận siêu tối đa nhân như là 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: 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: 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. - Robert S. Barnes
@Nils Pipenbrinck: Tôi đã chạy thử nghiệm của bạn. Khi tôi biên dịch với -O3, tôi nhận được kết quả tương tự như bạn, về tốc độ tăng 20%. Nhưng khi tôi biên dịch mà không có bất kỳ cờ tối ưu hóa nào thì cả hai chạy giống hệt nhau. Điều này thật thú vị: Không có cờ Op = 0m5.022 cho cả hai, -O3 0m3.186s & 0m2.583s, -Os 0m2.391s & 0m2.314s. Tối ưu hóa cho kích thước cho kết quả tốt nhất. Thêm giới hạn trong trường hợp này chỉ mua thêm 3,2% hiệu suất. Tự hỏi tại sao vậy? - Robert S. Barnes


Bài viết Làm sáng tỏ từ khóa hạn chế đề cập đến bài báo Tại sao Bí danh được lập trình chỉ định là một ý tưởng tồi (pdf) mà nói nó thường không giúp đỡ và cung cấp các phép đo để sao lưu này.


6
2018-05-15 20:18



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


Từ khóa hạn chế có cung cấp các lợi ích đáng kể trong gcc / g ++ không?

có thể giảm số lượng hướng dẫn như được hiển thị trên 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

Đầu vào:

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

Biên dịch và dịch ngược:

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

Với -O0, họ 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 không được khởi xướng, quy ước gọi điện Là:

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

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

Tất nhiên, hướng dẫn có thể có độ trễ khác nhau, nhưng điều này cho một ý tưởng tốt.

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

Đoạn mã trên được lấy từ Ví dụ Wikipedia đó là rất chiếu sáng.

Lắp ráp giả 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

Dành cho 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

Có thực sự nhanh hơn không?

Ermmm ... không phải cho thử nghiệm đơ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 CPU AMD64 Intel i5-3210M.

Tôi thú nhận rằng tôi vẫn không hiểu các CPU hiện đại. Cho tôi biết nếu bạn:

  • 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 kiểm tra lắp ráp, nơi nó trở nên nhanh hơn nhiều
  • hiểu tại sao không có sự khác biệt

4
2018-06-14 10:43





tôi đã thử nghiệm điều này Chương trình C. Không có restrict phải mất 12,640 giây để hoàn thành, với restrict 12.516. Có vẻ như nó có thể tiết kiệm một số thời gian.


0
2017-12-27 09:33



Đó là sự khác biệt trong tiếng ồn đo lường ... - Drew Hall
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 lúc này trình biên dịch có thể xem xét rằng * a * b và * inc có thể đã bị thay đổi. - James
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. - Nils Pipenbrinck
Nhưng nếu bạn vô hiệu hóa nội tuyến, bạn giả tạo làm tê liệt trình biên dịch, vì vậy bạn không còn nhận được một bức tranh chính xác về bao nhiêu restrictgiúp mã thực tế. - jalf
@raphaelr: Có vẻ như bạn cần sử dụng cờ tối ưu hóa để hạn chế hữu ích. Hãy thử hoặc là -O3 hoặc -Os. - Robert S. Barnes


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


0
2017-12-27 09:42



Trên thực tế, nếu bạn đọc xuống trang, bạn sẽ nhận thấy rằng mặc dù 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 ++. - Robert S. Barnes
@ Robert: Và 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. Do đó, __restrict__ không nên xung đột với bất kỳ người dùng nào được khai báo định danh. - Martin York
@ Martin: Làm thế nào để 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. - Robert S. Barnes
Tôi đồng ý rằng nó không rõ ràng, nhưng nó có vẻ không phù hợp để bỏ qua restrict nhưng không __restrict__. Dù bằng cách nào, nó vẫn không di động, và có lợi trong trường hợp rất cụ thể. Tôi đề nghị rằng nếu bạn biết nó có lợi trong một tình huống cụ thể, và bạn cần lợi ích đó để đạt được thành công, sau đó sử dụng nó; nếu không thì tại sao làm cho mã không vô cùng di động? Tôi sẽ không sử dụng nó thường xuyên, nhưng như là một phương sách cuối cùng và sau khi thử nghiệm lợi ích thực tế. - Clifford
@Clifford: Tất nhiên, nhưng nó là như thế với khá nhiều bất kỳ tối ưu hóa - thử nghiệm theo cách này và theo cách đó và sau đó sử dụng những gì làm việc. - Robert S. Barnes