如何理解C語言關鍵字restrict?
對教材上的解釋不是很理解。這個關鍵字的作用是什麼,以及在什麼情況下使用?
要理解 restrict,先要知道什麼是 Pointer aliasing。Pointer aliasing 是指兩個或以上的指針指向同一數據,例如
int i = 0;
int *a = i;
int *b = i;
這樣會有什麼問題呢?
如果編譯器採用最安全的假設,即不理會兩個指針會否指向同一數據,那麼通過指針讀寫數據是很直觀的。
然而,這種假設會令編譯器無法優化,例如:int foo(int *a, int *b)
{
*a = 5;
*b = 6;
return *a + *b; // 不一定是 11!
}
如果 a 和 b 都指向同一數據,*b = 6 會導致 *a = 6,返回12。所以編譯器在做 *a + *b 的時候,需要重新讀取 *a 指向的數據:
foo:
movl $5, (%rdi) # 存儲 5 至 *a
movl $6, (%rsi) # 存儲 6 至 *b
movl (%rdi), %eax # 重新讀取 *a (因為有可能被上一行指令造成改變)
addl $6, %eax # 加上 6
ret
int rfoo(int *restrict a, int *restrict b)
{
*a = 5;
*b = 6;
return *a + *b;
}
rfoo:
movl $11, %eax # 在編譯期已計算出 11
movl $5, (%rdi) # 存儲 5 至 *a
movl $6, (%rsi) # 存儲 6 至 *b
ret
但如果用了 restrict 去修飾兩個指針,而它們在作用域內又指向同一地址,那麼是未定義行為。
總括而言,restrict 是為了告訴編譯器額外信息(兩個指針不指向同一數據),從而生成更優化的機器碼。注意,編譯器是無法自行在編譯期檢測兩個指針是否 alias。如使用 restrict,程序員也要遵守契約才能得出正確的代碼(指針不能指向相同數據)。
以個人經驗而言,編寫代碼時通常會忽略 pointer aliasing 的問題。更常見是在性能剖測時,通過反編譯看到很多冗餘的讀取指令,才會想到加入 restrict 關鍵字來提升性能。
本答案的例子來自restrict type qualifier。從cache說起。機械硬碟操作太慢,操作系統就利用內存做硬碟內容的cache。內存太慢,CPU就提供了多級cache,一級二級三級。
如果我想榨乾最後一點性能怎麼辦?拿寄存器做cache。我本來要對內存讀10次,但是如果這10次讀取都是讀同一個值的話,不妨用一個寄存器cache起來,省去了額外的內存讀取。
可是我不知道內存里的東西什麼時候變啊。如果別人修改了內存,我怎麼知道寄存器里的值已經是舊的了呢?
這裡對「別人」這個詞做點解釋。由於歷史原因,現在的C或C++編譯器編譯一個函數的時候,是把本函數看作「自己」,把其餘所有函數看作「別人」的。所以哪怕是在自己這個函數里調用了另一個全局函數,也算是調用了「別人」的代碼,發生了什麼幾乎一概不知,也就不敢保證內存哪些地方被動過,哪些地方沒動過。
restrict的定義是 ( restrict type qualifier ):
During each execution of a block in which a restricted pointer P is declared (typically each execution of a function body in which P is a function parameter), if some object that is accessible through P (directly or indirectly) is modified, by any means, then all accesses to that object (both reads and writes) in that block must occur through P (directly or indirectly), otherwise the behavior is undefined.
現在程序員用restrict修飾一個指針,意思就是「只要這個指針活著,我保證這個指針獨享這片內存,沒有『別人』可以修改這個指針指向的這片內存,所有修改都得通過這個指針來」。由於這個指針的生命周期是已知的,編譯器可以放心大膽地把這片內存中前若干位元組用寄存器cache起來。
如果不管不顧由於歷史原因而沿襲的C/C++編譯模型(object file + linker),做暴力的whole program analysis 或者 LTO 的話,restrict的提供的信息多半能推導出十之七八,但是應該做不到完美。靜態檢查嘛,perfectly sound,perfectly complete和always halt最多三選二。推薦閱讀:
※C 語言中不同類型指針的大小是否完全相同,為什麼?
※指針的指針定義為什麼用int ** ptr,而不是int *ptr?
※C語言如何封裝printf函數?
※C語言結構體內部的函數指針有什麼意義?
※一條C語言語句不一定是原子操作,但是一個彙編指令是原子操作嗎?