2009-04-14 37 views
136

我正在瀏覽一些文檔和問題/答案,並看到它提到。我讀了一個簡短的描述,指出這基本上是程序員的一個承諾,即指針不會被用來指向別的地方。C99'restrict'關鍵字的現實用法?

任何人都可以提供一些實際的情況下,其實際使用它的價值?

+2

`memcpy` vs`memmove`就是一個典型的例子。 – 2016-04-19 18:22:43

+0

@AlexandreC:我不認爲這是一個特別適用的方法,因爲缺少「限制」限定符並不意味着程序邏輯將與重載源和目標一起工作,也不會存在這樣的限定符來阻止所謂的方法從確定源和目的地是否重疊,如果是,將dest替換爲src +(dest-src),因爲它是從src派生的,所以可以將其替換爲別名。 – supercat 2016-04-19 22:25:34

+0

@supercat:這就是爲什麼我把它作爲評論。但是,1)對memcpy進行`限制`限定的參數原則上允許一個幼稚的實現被積極地優化,2)僅僅調用`memcpy`就能讓編譯器假定賦給它的參數不是別名,允許圍繞`memcpy`調用進行一些優化。 – 2016-04-20 06:50:33

回答

147

restrict表示指針是訪問底層對象的唯一東西。它消除了指針混疊的可能性,使編譯器能夠更好地進行優化。

舉例來說,假設我有可以乘號的向量存儲器專用指令的機器,我有以下代碼:

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

編譯器需要妥善處理如果dest,SRC1和src2重疊,這意味着它必須從開始到結束一次執行一次乘法。通過使用restrict,編譯器可以自由地優化此代碼以使用向量指令。

編輯:維基百科有一個條目restrict,另一個例子,here

95

Wikipedia example非常照明。

它清楚地顯示了它如何允許保存一個彙編指令

沒有限制:

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

僞組件:

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 

隨着限制:

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

僞組件:

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 

GCC真的做到了嗎?

GCC 4.8的Linux的x86-64:

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

隨着-O0,它們是相同的。

隨着-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) 

對於外行來說,所述calling convention是:

  • rdi =第一參數
  • rsi =第二參數
  • rdx =第三參數

GCC輸出比維基文章更清晰:4條指令vs 3條指令。

陣列

到目前爲止,我們有單指令積蓄,但如果指針表示數組被掛繞,一個常見的情況,然後一串指令可以保存,如supercat提及。

考慮例如:

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

因爲restrict,智能編譯器(或人)的,可以優化,爲:

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

這是可能效率更高,因爲它可以是彙編(如glibc)優化:Is it better to use std::memcpy() or std::copy() in terms to performance?

GCC真的這樣做嗎?

GCC 5.2.1.Linux X86-64的Ubuntu 15.10:

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

隨着-O0,兩者是相同的。

隨着-O3

  • 與限制:

    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 
    

    兩個memset調用預期。

  • 沒有限制:無STDLIB電話,只是一個16迭代寬loop unrolling我不打算在這裏重現:-)

我已經沒有耐心標杆他們,但我相信限制版本會更快。

C99

讓我們來看看完整性的考慮標準。

restrict表示兩個指針不能指向重疊的內存區域。最常用的用法是函數參數。

這限制了函數如何被調用,但允許更多的編譯時優化。

如果調用者沒有遵循restrict合同,未定義的行爲。

C99 N1256 draft 6.7.3/7「類型限定符」雲:

的預期用途限制限定符(如寄存器存儲類)是促進優化,並刪除限定符的所有實例從所有組成符合程序的預處理翻譯單元都不會改變其含義(即可觀察到的行爲)。

和6.7.3.1「限制的正式定義」給出了細節。

嚴格別名規則

restrict關鍵字隻影響兼容類型的指針(例如兩個int*),因爲嚴格別名規則說走樣不兼容的類型默認情況下未定義行爲,所以編譯器可以假定它沒有發生和優化。

參見:What is the strict aliasing rule?

參見