Sử dụng thực tế từ khóa 'hạn chế' C99?


183

Tôi đã duyệt qua một số tài liệu và câu hỏi / câu trả lời và thấy nó được đề cập. Tôi đã đọc một mô tả ngắn gọn, nói rằng về cơ bản, đó sẽ là một lời hứa từ lập trình viên rằng con trỏ sẽ không được sử dụng để chỉ ra một nơi khác.

Bất cứ ai cũng có thể cung cấp một số trường hợp thực tế mà giá trị của nó thực sự sử dụng này?


4
memcpyvs memmovelà một ví dụ điển hình.
Alexandre C.

@AlexandreC.: Tôi không nghĩ đó là một ứng dụng đặc biệt, vì thiếu vòng loại "hạn chế" không ngụ ý rằng logic chương trình sẽ hoạt động với quá tải nguồn và đích, cũng như sự hiện diện của vòng loại đó sẽ ngăn phương thức được gọi từ xác định xem có trùng nhau nguồn và đích hay không, và nếu vậy, thay thế mệnh đề bằng src + (Dest-src), vì nó có nguồn gốc từ src, sẽ được phép đặt bí danh cho nó.
supercat

@supercat: Đó là lý do tại sao tôi đặt nó như một bình luận. Tuy nhiên, về nguyên tắc, các restrictđối số đủ điều kiện để memcpycho phép triển khai một cách ngây thơ được tối ưu hóa mạnh mẽ và 2) việc gọi đơn thuần memcpycho phép trình biên dịch giả định rằng các đối số được cung cấp cho nó không bí danh, có thể cho phép tối ưu hóa memcpycuộc gọi.
Alexandre C.

@AlexandreC.: Sẽ rất khó để trình biên dịch trên hầu hết các nền tảng có thể tối ưu hóa một memcpy ngây thơ - ngay cả với "hạn chế" - ở bất kỳ nơi nào hiệu quả như phiên bản phù hợp với mục tiêu. Tối ưu hóa phía cuộc gọi sẽ không yêu cầu từ khóa "hạn chế" và trong một số trường hợp, các nỗ lực nhằm tạo điều kiện cho những điều đó có thể phản tác dụng. Ví dụ, nhiều triển khai của memcpy có thể, với chi phí bổ sung bằng 0, được coi memcpy(anything, anything, 0);là không hoạt động và đảm bảo rằng nếu plà một con trỏ tới các nbyte ít nhất có thể ghi , memcpy(p,p,n); sẽ không có tác dụng phụ bất lợi. Những trường hợp như vậy có thể phát sinh ...
supercat

... một cách tự nhiên trong một số loại mã ứng dụng nhất định (ví dụ như một thói quen sắp xếp một vật phẩm với chính nó) và trong các triển khai khi chúng không có tác dụng phụ bất lợi, hãy để các trường hợp đó được xử lý bởi mã trường hợp chung có thể hiệu quả hơn so với việc có để thêm các bài kiểm tra trường hợp đặc biệt. Thật không may, một số người viết trình biên dịch dường như nghĩ rằng tốt hơn là yêu cầu các lập trình viên thêm mã trình biên dịch có thể không thể tối ưu hóa, để tạo điều kiện cho "cơ hội tối ưu hóa" mà trình biên dịch sẽ hiếm khi khai thác.
supercat

Câu trả lời:


182

restrictnói rằng con trỏ là thứ duy nhất truy cập vào đối tượng bên dưới. Nó loại bỏ khả năng răng cưa con trỏ, cho phép trình biên dịch tối ưu hóa tốt hơn.

Chẳng hạn, giả sử tôi có một máy có các hướng dẫn chuyên biệt có thể nhân các vectơ số trong bộ nhớ và tôi có mã sau:

void MultiplyArrays(int* dest, int* src1, int* src2, int n)
{
    for(int i = 0; i < n; i++)
    {
        dest[i] = src1[i]*src2[i];
    }
}

Trình biên dịch cần xử lý đúng nếu dest, src1src2chồng lấp, nghĩa là nó phải thực hiện một phép nhân tại một thời điểm, từ đầu đến cuối. Bằng cách có restrict, trình biên dịch có thể tự do tối ưu hóa mã này bằng cách sử dụng các hướng dẫn vectơ.

Wikipedia có một mục trên restrict, với một ví dụ khác, ở đây .


3
@Michael - Nếu tôi không nhầm, thì vấn đề sẽ chỉ xảy ra khi destchồng chéo một trong hai vectơ nguồn. Tại sao sẽ có một vấn đề nếu src1src2chồng chéo?
ysap

1
hạn chế thông thường chỉ có hiệu lực khi chỉ vào một đối tượng được sửa đổi, trong trường hợp đó, nó khẳng định không có tác dụng phụ ẩn nào cần được tính đến. Hầu hết các trình biên dịch sử dụng nó để tạo điều kiện cho vector hóa. Msvc sử dụng kiểm tra thời gian chạy cho dữ liệu chồng chéo cho mục đích đó.
tim18

Thêm từ khóa đăng ký vào biến vòng lặp for cũng làm cho nó nhanh hơn ngoài việc thêm hạn chế.

2
Trên thực tế, từ khóa đăng ký chỉ là tư vấn. Và trong các trình biên dịch kể từ khoảng năm 2000, i (và n để so sánh) trong ví dụ sẽ được tối ưu hóa thành một thanh ghi cho dù bạn có sử dụng từ khóa đăng ký hay không.
Đánh dấu Fischler

154

Các ví dụ Wikipediarất sáng.

Nó cho thấy rõ làm thế nào nó cho phép lưu một hướng dẫn lắp ráp .

Không giới hạn:

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

Lắp ráp giả:

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 x may point to a (a aliased by x) thus 
; the value of x will change when the value of a
; changes.
load R1  *x
load R2  *b
add R2 += R1
set R2  *b

Với hạn chế:

void fr(int *restrict a, int *restrict b, int *restrict x);

Lắp ráp giả:

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" is no longer needed.
load R2  *b
add R2 += R1
set R2  *b

GCC có thực sự làm điều đó?

GCC 4.8 Linux x86-64:

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

Với -O0, họ là như 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 quen biết, quy ước gọi là:

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

Đầu ra GCC thậm chí còn rõ ràng hơn bài viết wiki: 4 hướng dẫn so với 3 hướng dẫn.

Mảng

Cho đến nay chúng ta có các khoản tiết kiệm lệnh đơn, nhưng nếu con trỏ biểu thị các mảng được lặp lại, một trường hợp sử dụng phổ biến, thì một loạt các hướng dẫn có thể được lưu, như được đề cập bởi supercat .

Xem xét ví dụ:

void f(char *restrict p1, char *restrict p2) {
    for (int i = 0; i < 50; i++) {
        p1[i] = 4;
        p2[i] = 9;
    }
}

Bởi vì restrict, một trình biên dịch thông minh (hoặc con người), có thể tối ưu hóa điều đó thành:

memset(p1, 4, 50);
memset(p2, 9, 50);

có khả năng hiệu quả hơn nhiều vì nó có thể được tối ưu hóa khi triển khai libc (như glibc): Sử dụng std :: memcpy () hoặc std :: copy () để thực hiện tốt hơn?

GCC có thực sự làm điều đó?

GCC 5.2.1.Linux x86-64 Ubuntu 15.10:

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

Với -O0, cả hai đều giống nhau.

Với -O3:

  • với hạn chế:

    3f0:   48 85 d2                test   %rdx,%rdx
    3f3:   74 33                   je     428 <fr+0x38>
    3f5:   55                      push   %rbp
    3f6:   53                      push   %rbx
    3f7:   48 89 f5                mov    %rsi,%rbp
    3fa:   be 04 00 00 00          mov    $0x4,%esi
    3ff:   48 89 d3                mov    %rdx,%rbx
    402:   48 83 ec 08             sub    $0x8,%rsp
    406:   e8 00 00 00 00          callq  40b <fr+0x1b>
                            407: R_X86_64_PC32      memset-0x4
    40b:   48 83 c4 08             add    $0x8,%rsp
    40f:   48 89 da                mov    %rbx,%rdx
    412:   48 89 ef                mov    %rbp,%rdi
    415:   5b                      pop    %rbx
    416:   5d                      pop    %rbp
    417:   be 09 00 00 00          mov    $0x9,%esi
    41c:   e9 00 00 00 00          jmpq   421 <fr+0x31>
                            41d: R_X86_64_PC32      memset-0x4
    421:   0f 1f 80 00 00 00 00    nopl   0x0(%rax)
    428:   f3 c3                   repz retq

    Hai memsetcuộc gọi như mong đợi.

  • không hạn chế: không có cuộc gọi stdlib, chỉ một vòng lặp rộng 16 vòng lặp không kiểm soát mà tôi không có ý định sao chép ở đây :-)

Tôi không đủ kiên nhẫn để đánh giá chúng, nhưng tôi tin rằng phiên bản giới hạn sẽ nhanh hơn.

C99

Chúng ta hãy nhìn vào tiêu chuẩn cho sự hoàn thiện vì lợi ích.

restrictnói rằng hai con trỏ không thể trỏ đến các vùng bộ nhớ chồng chéo. Việc sử dụng phổ biến nhất là cho các đối số chức năng.

Điều này hạn chế cách hàm có thể được gọi, nhưng cho phép tối ưu hóa thời gian biên dịch nhiều hơn.

Nếu người gọi không tuân theo restricthợp đồng, hành vi không xác định.

Bản dự thảo C99 N1256 6.7.3 / 7 "Loại vòng loại" cho biết:

Mục đích sử dụng của vòng loại hạn chế (như lớp lưu trữ đăng ký) là để thúc đẩy tối ưu hóa và xóa tất cả các phiên bản của vòng loại khỏi tất cả các đơn vị dịch tiền xử lý soạn thảo chương trình tuân thủ không thay đổi ý nghĩa của nó (nghĩa là hành vi có thể quan sát được).

và 6.7.3.1 "Định nghĩa chính thức về hạn chế" đưa ra các chi tiết chính.

Quy tắc răng cưa nghiêm ngặt

Các restricttừ khóa chỉ ảnh hưởng đến con trỏ của các loại tương thích (ví dụ như hai int*) bởi vì các quy tắc nghiêm ngặt răng cưa nói rằng răng cưa loại không tương thích là hành vi không xác định theo mặc định, và do đó trình biên dịch có thể giả định nó không xảy ra và tối ưu hóa đi.

Xem: quy tắc răng cưa nghiêm ngặt là gì?

Xem thêm


9
Vòng loại "hạn chế" thực sự có thể cho phép tiết kiệm lớn hơn nhiều. Ví dụ, được đưa ra void zap(char *restrict p1, char *restrict p2) { for (int i=0; i<50; i++) { p1[i] = 4; p2[i] = 9; } }, các vòng loại hạn chế sẽ cho phép trình biên dịch viết lại mã thành "memset (p1,4,50); memset (p2,9,50);". Restrict rất vượt trội so với răng cưa dựa trên kiểu; đó là một trình biên dịch xấu hổ tập trung nhiều hơn vào phần sau.
supercat

@supercat ví dụ tuyệt vời, thêm vào để trả lời.
Ciro Santilli 郝海东 冠状 病 事件 法轮功

2
@ tim18: Từ khóa "hạn chế" có thể cho phép nhiều tối ưu hóa mà thậm chí tối ưu hóa dựa trên loại tích cực cũng không thể. Hơn nữa, sự tồn tại của "hạn chế" trong ngôn ngữ - không giống như bí danh dựa trên kiểu tích cực - không bao giờ khiến bạn không thể hoàn thành các nhiệm vụ một cách hiệu quả nhất có thể khi vắng mặt (vì mã sẽ bị phá vỡ bởi "hạn chế" có thể đơn giản không sử dụng nó, trong khi mã bị phá vỡ bởi TBAA tích cực thường phải được viết lại theo cách kém hiệu quả hơn).
23/5/2016

2
@ tim18: Bao quanh những thứ có chứa gạch chân kép trong backticks, như trong __restrict. Mặt khác, gạch chân kép có thể bị hiểu sai là một dấu hiệu cho thấy bạn đang hét.
supercat

1
Điều quan trọng hơn là không la hét là phần dưới có ý nghĩa liên quan trực tiếp đến điểm bạn đang cố gắng thực hiện.
xe máy
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.