程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C >> 關於C >> C語言中的restrict,const,volatile

C語言中的restrict,const,volatile

編輯:關於C

1.restrict

C語言中的一種類型限定符(Type Qualifiers),用於告訴編譯器,對象已經被指針所引用,不能通過除該指針外所有其他直接或間接的方式修改該對象的內容。

restrict是c99標准引入的,它只可以用於限定和約束指針,並表明指針是訪問一個數據對象的唯一且初始的方式.即它告訴編譯器,所有修改該指針所指向內存中內容的操作都必須通過該指針來修改,而不能通過其它途徑(其它變量或指針)來修改;這樣做的好處是,能幫助編譯器進行更好的優化代碼,生成更有效率的匯編代碼.如 int *restrict ptr, ptr 指向的內存單元只能被 ptr 訪問到,任何同樣指向這個內存單元的其他指針都是未定義的,直白點就是無效指針。restrict 的出現是因為 C 語言本身固有的缺陷,C 程序員應當主動地規避這個缺陷,而編譯器也會很配合地優化你的代碼.

 

考慮下面的例子: int ar[10]; int * restrict restar=(int *)malloc(10*sizeof(int)); int *par=ar; 這裡說明restar是訪問由malloc()分配的內存的唯一且初始的方式。par就不是了。 那麼: for(n=0;n<10;n++) { par[n]+=5; restar[n]+=5; ar[n]*=2; par[n]+=3; restar[n]+=3; } 因為restar是訪問分配的內存的唯一且初始的方式,那麼編譯器可以將上述對restar的操作進行優化: restar[n]+=8; 而par並不是訪問數組ar的唯一方式,因此並不能進行下面的優化: par[n]+=8; 因為在par[n]+=3前,ar[n]*=2進行了改變。使用了關鍵字restrict,編譯器就可以放心地進行優化了。 C庫中有兩個函數可以從一個位置把字節復制到另一個位置。在C99標准下,它們的原型如下: void * memcpy(void * restrict s1, const void * restrict s2,size_tn); void * memmove(void * s1, const void * s2, size_t n); 這兩個函數均從s2指向的位置復制n字節數據到s1指向的位置,且均返回s1的值。兩者之間的差別由關鍵字restrict造成,即memcpy()可以假定兩個內存區域沒有重疊。memmove()函數則不做這個假定,因此,復制過程類似於首先將所有字節復制到一個臨時緩沖區,然後再復制到最終目的地。如果兩個區域存在重疊時使用memcpy()會怎樣?其行為是不可預知的,既可以正常工作,也可能失敗。在不應該使用memcpy()時,編譯器不會禁止使用memcpy()。因此,使用memcpy()時,您必須確保沒有重疊區域。這是程序員的任務的一部分。 關鍵字restrict有兩個讀者。一個是編譯器,它告訴編譯器可以自由地做一些有關優化的假定。另一個讀者是用戶,他告訴用戶僅使用滿足restrict要求的參數。一般,編譯器無法檢查您是否遵循了這一限制,如果您蔑視它也就是在讓自己冒險。 引用:http://tonybai.com/2011/11/18/also-talk-about-restrict-type-qualifier-in-c/ 為何C標准委員會要在C99標准中引入restrict呢?這當然是有歷史原因的。我們先來看看下面這個例子:

 

/* foo.c */
void foo(int *p, int *q, int *r) {
*p += *r;
*q += *r ;
}

int main() {
int a = 1;
int b = 2;
int c = 3;
foo(&a, &b, &c);
}

C語言的設計哲學之一就是性能至上,為了性能可以捨棄一切。C程序員都希望編譯器能為自己編寫的程序生成高性能的目標代碼,我們現在就來看看GCC編譯器(在優化開關-O2已打開的情況下)為這段程序生成的目標代碼是什麼樣子的。

我們通過GDB對函數foo進行反匯編,結果如下:

(gdb) disas foo
Dump of assembler code for function foo:
0x080483c0 : push %ebp
0x080483c1 : mov %esp,%ebp
0x080483c3 : mov 0×10(%ebp),%edx
0x080483c6 : mov 0×8(%ebp),%ecx
0x080483c9 : mov 0xc(%ebp),%eax
0x080483cc : push %ebx
0x080483cd : mov (%edx),%ebx
0x080483cf : add %ebx,(%ecx)
0x080483d1 : mov (%edx),%edx
0x080483d3 : add %edx,(%eax)
0x080483d5 : pop %ebx
0x080483d6 : pop %ebp
0x080483d7 : ret
End of assembler dump.

這段匯編代碼不是很難,我們將關鍵部分抽取出來並在每行匯編碼後面給出解釋:
mov 0×10(%ebp),%edx ; r -> %edx,將指針r指向的內存對象的地址放入寄存器edx
mov 0×8(%ebp),%ecx ; p -> %ecx,將指針p指向的內存對象的地址放入寄存器ecx
mov 0xc(%ebp),%eax ; q -> %eax,將指針q指向的內存對象的地址放入寄存器eax
push %ebx
mov (%edx),%ebx ; *r -> %ebx,將指針r指向的內存對象的值加載到寄存器ebx中
add %ebx,(%ecx) ; *r + *p -> *p, 將寄存器ebx中的數值與指針p所指內存對象的值相加,結果存放在指針p所指的內存對象中
mov (%edx),%edx ; *r -> %edx,將指針r指向的內存對象的值加載到寄存器edx中
add %edx,(%eax) ; *r + *q -> *q,將寄存器edx中的數值與指針q所指內存對象的值相加,結果存放在指針q所指的內存對象中

這段匯編代碼是否是經過優化過的呢?我們結合foo函數的源代碼分析後可以發現生成的目標碼並非是經過優化的。在foo函數中指針r指向的內存對象一直都作為右值,其值沒有被改動,編譯器在第二次加法操作中完全可以直接利用第一次加載*r值的寄存器,而不是重新從內存中加載*r。但編譯器為何沒有優化掉這次訪存操作呢?原因就在於編譯器憑借C源代碼中已有的信息是無法作出這種優化決策的。因為當編譯器在foo的實現的上下文中看到三個指針時,它並不能判斷出這三個指針所指向的地址是否有重疊,也就是說編譯器並不能確定在第二次加法操作之前,r指向的內存對象是否被改變,編譯器只能中規中矩地生成未經優化的目標代碼,即每次都重新加載*r到寄存器,否則擅自優化會導致一些不可預期的行為。

那如何能幫助編譯器作出正確的優化決策呢?這就需要程序員顯式地為編譯器提供用於決策的信息。在C99以前,很多編譯器通過提供#Pragma參數或自擴展的關鍵字來實現這一點。比如:GCC為程序員提供了__restrict__或__restrict擴展關鍵字,有了這些關鍵字後,C程序員就可以顯式地向編譯器傳達信息了。還以foo為例,我們看看加上__restrict__後編譯器為函數foo生成的目標代碼是什麼樣子的:

void foo(int *__restrict__ p, int *__restrict__ q, int * __restrict__r) {
*p += *r;
*q += *r ;
}

(gdb) disas foo
Dump of assembler code for function foo:
0x080483c0 : push %ebp
0x080483c1 : mov %esp,%ebp
0x080483c3 : mov 0×10(%ebp),%edx
0x080483c6 : mov 0×8(%ebp),%ecx
0x080483c9 : mov 0xc(%ebp),%eax
0x080483cc : mov (%edx),%edx
0x080483ce : add %edx,(%ecx)
0x080483d0 : add %edx,(%eax)
0x080483d2 : pop %ebp
0x080483d3 : ret
End of assembler dump.

我們主要來看下面連續的三行匯編代碼:
0x080483cc : mov (%edx),%edx ; *r -> %edx,將指針r指向的內存對象的值加載到寄存器edx中
0x080483ce : add %edx,(%ecx) ; *r + *p -> *p,將寄存器edx中的數值與指針p所指內存對象的值相加,結果存放在指針p所指的內存對象中
0x080483d0 : add %edx,(%eax) ; *r + *q -> *q,將寄存器edx中的數值與指針q所指內存對象的值相加,結果存放在指針q所指的內存對象中

可以看到這次編譯器生成了優化後的代碼,第二次加法操作直接用的是緩存在寄存器中的*r值。以上就是C99引入restrict關鍵字的一個基本考慮,通過restrict,C程序員可以告知編譯器大膽地去執行優化,程序員來保證代碼符合restrict語義的約束要求,這可以看作是一種程序員與編譯器間的契約。

前面說過restrict是一種類型修飾符,但不同於其他兩種修飾符const和volatile,restrict僅用於修飾指針類型與不完整類型(incomplete types),C99規范中對restrict的诠釋是這樣的:"Types other than pointer types derived from object or incomplete types shall not be restrict-qualified"。用restrict修飾指針是最常見的情況,被restrict修飾的指針到底有何與眾不同呢?

用restrict修飾某指針變量意味著在該指針變量的生命周期內,該指針是其所指內存對象的唯一訪問和修改入口,即所有對其所指的內存對象數據的訪問和修改都是通過該指針完成的。或是說在特定上下文中該指針所指的內存對象不存在別名(Alias)。何為別名?引用同一內存對象的多個變量互為別名。比如:
int a = 5;
int *p = &a;
int *q = p;

這樣p, q, a互為別名,它們都引用到地址&a。另外如果兩個指針所指向的內存對象有相互重疊,那相互也算做是一種別名。

restrict的語義約束可以分成兩個方面,一個是對內部的,一個是對外部的。我們還以上面的foo函數為例,這裡稍作改動,去掉p,q兩個參數的restrict修飾:

void foo(int *p, int *q, int *restrict r) {
*p += *r;
*q += *r ;
}

從foo內部來看,r是一個被restrict修飾的指針,其生命周期從foo執行開始一直到foo執行結束。按照上面對restrict的诠釋,在foo函數內部不應該存在指針r所指內存對象的別名,即不應該存在下面情況:

void foo(int *p, int *q, int *restrict r) {
int *z = r;
…later, use r and z…
}

這的約束是foo的實現者保證的。

對於外部而言,即foo的使用者依然要保證傳入實參後p或q不是r所指內存對象的別名,下面這樣的代碼將違反約束:
int a = 5;
int b = 6;
foo(&a, &b, &b);

這裡還有一個問題:雖然r用了restrict修飾符,但編譯器在看到void foo(int *p, int *q, int *restrict r)這個函數原型後就一定會生成優化的代碼嗎?顯然通過這個原型信息,編譯器依舊無法保證p或q不是r所指內存地址的別名,所以對上面這段代碼編譯器無法給出優化,即使r是被restrict修飾的,至少在我的Ubuntu gcc4.4.3上是不會生成優化目標代碼的。也就是說這個例子中foo的設計者與編譯器之間的契約不夠充分,無法讓Compiler完全信服地去執行優化。這就需要進一步的補充契約,也就是讓Compiler意識到p, q, r在foo中都是各自所指內存地址的唯一入口,為了達到這一點,我們只能為p, q也加上restrict修飾,這樣契約變成foo內部的p, q,r是給自所指內存的唯一入口,p, q, r也就不可能是對方的別名了。

但即使所有指針參數都加上restrict修飾,Compiler就一定會生成優化的代碼嗎,事實是也不一定。看下面例子:
void foo1(int *restrict p, int *restrict q, char *restrict r) {
*p += (int)*r;
*q += (int)*r;
}
void foo2(int *restrict p, int *restrict q, long long int *restrict r) {
*p += (int)*r;
*q += (int)*r;
}

可以看到我們分別將foo函數的最後一個參數r的類型換為了char*和long long int*並,形成兩個函數foo1和foo2,我們嘗試用GCC生成對應的目標代碼,通過反編譯,我們可以得到如下結果:

(gdb) disas foo1
Dump of assembler code for function foo1:
0×08048430 : push %ebp
0×08048431 : mov %esp,%ebp
0×08048433 : mov 0×10(%ebp),%edx
0×08048436 : mov 0×8(%ebp),%ecx
0×08048439 : mov 0xc(%ebp),%eax
0x0804843c : push %ebx
0x0804843d : movsbl (%edx),%ebx
0×08048440 : add %ebx,(%ecx)
0×08048442 : movsbl (%edx),%edx
0×08048445 : add %edx,(%eax)
0×08048447 : pop %ebx
0×08048448 : pop %ebp
0×08048449 : ret
End of assembler dump.

(gdb) disas foo2
Dump of assembler code for function foo2:
0×08048450 : push %ebp
0×08048451 : mov %esp,%ebp
0×08048453 : mov 0×10(%ebp),%edx
0×08048456 : mov 0×8(%ebp),%ecx
0×08048459 : mov 0xc(%ebp),%eax
0x0804845c : mov (%edx),%edx
0x0804845e : add %edx,(%ecx)
0×08048460 : add %edx,(%eax)
0×08048462 : pop %ebp
0×08048463 : ret
End of assembler dump.

我們可以看到GCC只為foo2生成了優化後的代碼,而foo1並未被優化。這個結果讓人有些摸不著頭腦。難道編譯器認為char*指針有成為int*指針所指對象的alias的潛在可能,而int*指針無法成為long long int*指針所指對象的alias?在C99規范中我也沒能找到解釋這一現象的答案。看來即使增加了restrict,編譯器也是有選擇的信任,至少Gcc是這樣的。

restrict的作用范圍與其修飾的指針的生命周期一致,你可以聲明文件作用域(file scope)的restrict指針變量,也可以在某個代碼block中使用restrict指針。如果某個結構體成員是restrict pointer類型,那該指針的生命周期就等同於該結構體實例的生命周期。

如果你惡意破壞你和Compiler之間的契約,別指望Compiler會有Warning提示,Compiler在這方面是完全信賴程序員的,不確定行為不可避免。比如:
void foo(int *restrict p, int *restrict q, int *restrict r) {
*p += *r;
*q += *r;
}

int main() {
int a = 1;
int b = 2;
int c = 3;
foo(&a, &b, &a);
printf("a = %d, b = %d, c = %d\n", a, b, c);
}
執行優化後的程序,我們得到的輸出為:
$ a.out
a = 2, b = 4, c = 3
這顯然與預期的a = 2, b = 3, c = 3不符,錯誤原因就在於你單方面違反了restrict契約。

C99規范中對restrict關鍵字的講解還算不少,甚至還給出了formal definition(C99 6.7.3.1),不過這個定義簡直就像一段天書,實在是晦澀難懂(《The New C Standard》一書對此有逐句的解釋,不過依舊很難理解)。另外restrict的存在對程序本身的語義沒有任何影響,對於不支持restrict的編譯器也大可忽略restrict修飾符。

至於在平時開發中如何使用restrict,我個人覺得最好是在有一定理解的前提下使用。這對C程序員能力還是有一定要求的。首先要明確你編寫的函數內部是否有可以優化的地方,如果根本沒有可優化的潛力,那使用restrict就畫蛇添足了;當然還有一種情況下你用restrict並不是期望編譯器給予優化,而是你的實現算法是基於參數指針所指內存對象無alias的前提的,你在函數原型中用restrict修飾參數主要是想將你的意圖告知該函數的使用者;第二要知道restrict對函數內部實現的約束,不要在內部實現時違反約束,導致未定義行為;第三如果你是一個使用者,面對采用了restrict修飾的函數接口,如void *memcpy(void * restrict s1, const void * restrict s2, size_t n),你要注意不能違反restrict約束,否則也會導致未定義行為。如果你是一個公共庫的開發者,你更應該盡量采用restrict,這對你的庫代碼的性能會是大有裨益的。

引用:http://www.jb51.net/article/42348.htm

2. const

 

變量聲明中帶有關鍵詞const,意味著不能通過賦值,增量或減量來修改該變量的值,這是顯而易見的一點。指針使用const則要稍微復雜點,因為不得不把讓指針本身成為const和指針指向的值成為const區別開來、下面的聲明表示pf指向的值必須是不變的

constfloat *pf;而pf則是可變的,它可以指向另外一個const或非const值;相反,下面的聲明說明pf是不能改變的,而pf所指向的值則是可以改變的:

float* const pf;

最後,當然可以有既不能改變指針的值也不能改變指針指向的值的值的聲明方式:

constfloat * const pf;

需要注意的是,還有第三種放置const關鍵字的方法:

float const * pf; //等價於constfloat * pf;

總結就是:一個位於*左邊任意位置的const使得數據成為常量,而一個位於*右邊的const使得指針本身成為const

還要注意的一點是關於const在全局數據中的使用:

使用全局變量被認為是一個冒險的方法,它使得數據在程序的任何部分都可以被錯誤地修改,如果數據是const,那麼這種擔心就是多余的了不是嘛?因此對全局數據使用const是合理的。

然而,在文件之間共享const數據要格外小心,有兩個策略可以使用。一個是遵循外部變量的慣用規則,在一個文件進行定義聲明,在其他文件進行引用聲明(使用關鍵字extern)。

/*file1.c------定義一些全局常量*/

const double PI = 3.14159;

/*file2.c-----是用在其他文件中定義的全局變量*/

extern const dounle PI;

另外一個方法是把全局變量放在一個include文件裡,這時候需要格外注意的是必須使用靜態外部存儲類

/*constant.h----定義一些全局常量*/

static const double PI = 3.14159;

/*file1.c-----使用其他文件定義的全局變量*/

#include”constant.h”。

/*file2.c-----使用其他文件定義的全局變量*/

#include”constant.h”

如果不使用關鍵字static,在文件file1.c和file2.c中包含constant.h將導致每個文件都有同一標識符的定義聲明ANSI標准不支持這樣做(有些編譯器確實支持)。通過使用static, 實際上給了每個文件一個獨立的數據拷貝,如果文件想使用該數據與另外一個文件通話,這樣做就不行了,因為每個文件只能看見他自己的拷貝,然而由於數據是不 可變的,這就不是問題了。使用頭文件的好處是不必惦記在一個文件中進行定義聲明,在另一個文件中進行引用聲明,缺點在於復制了數據,如果常量很大的話,這 就是個問題了。

3. volatile

限定詞volatile告訴編譯器,該變量除了可被程序改變意外還可以被其他代理改變。典型的它用於硬件地址和其他並行運行的程序共享的數據。例如,一個地址中可能保存著當前的時鐘信息。不管程序做些什麼,該地址會隨時間改變。另一種情況是一個地址用來接收來自其他計算機的信息;

語法同const:

volatile int a;//a是一個易變的位置

volatile int * pf;//pf指向一個易變的位置

把volatile作為一個關鍵字的原因是它可以方便編譯器優化。

假如有如下代碼:

va= x;

//一些不使用x的代碼

vb= x;

一個聰明的編譯器可能注意到你兩次使用了x,但是沒有改變它的值,它將把x臨時存貯在一個寄存器中,接著,當vb主要x是的時候,它從寄存器而非初始的內存位置得到x的值來節省時間。這個過程被稱為緩存。通常緩存是一個好的優化方式,但是如果兩個語句中間的其他代理改變了x的值的話就不是這樣了。如果沒有規定volatile關鍵字,編譯器將無從得知這種改變是否可能發生,因此,為了安全起見,編譯器不使用緩存。那是在ANSI以前的情形,現在,如果在聲明中沒有使用volatile關鍵字,編譯器就可以假定一個值在使用過程中沒有修改,它就可以試著優化代碼。總而言之,volatile使得每次讀取數據都是直接在內存讀取而不是緩存。

你可能會覺得奇怪,const和volatile可以同時使用,但是確實可以。例如硬件時鐘一般不能由程序改變,這使得他成為const,但他被程序以外的代理改變,這使得他成為volatile,所以你可以同時使用它們,順序是不重要的:

const volatile time;

volatile表明某個變量的值可能在外部被改變,優化器在用到這個變量時必須每次都小心地重新讀取這個變量的值,而不是使用保存在寄存器裡的備份。它可以適用於基礎類 型如:int,char,long......也適用於C的結構和C++的類。當對結構或者類對象使用volatile修飾的時候,結構或者類的所有成員 都會被視為volatile.

該關鍵字在多線程環境下經常使用,因為在編寫多線程的程序時,同一個變量可能被多個線程修改,而程序通過該變量同步各個線程。

簡單示例:

復制代碼代碼如下:
DWORD __stdcall threadFunc(LPVOID signal)
{
int* intSignal=reinterdivt_cast(signal);
*intSignal=2;
while(*intSignal!=1)
sleep(1000);
return 0;
}

該線程啟動時將intSignal 置為2,然後循環等待直到intSignal 為1 時退出。顯然intSignal的值必須在外部被改變,否則該線程不會退出。但是實際運行的時候該線程卻不會退出,即使在外部將它的值改為1,看一下對應的偽匯編代碼就明白了:

 

mov ax,signal
label:
if(ax!=1)
goto label

對於C編譯器來說,它並不知道這個值會被其他線程修改。自然就把它cache在寄存器裡面。C 編譯器是沒有線程概念的,這時候就需要用到volatile。volatile 的本意是指:這個值可能會在當前線程外部被改變。也就是說,我們要在threadFunc中的intSignal前面加上volatile關鍵字,這時 候,編譯器知道該變量的值會在外部改變,因此每次訪問該變量時會重新讀取,所作的循環變為如下面偽碼所示:
label:
mov ax,signal
if(ax!=1)
goto label

注意:一個參數既可以是const同時是volatile,是volatile因為它可能被意想不到地改變。它是const因為程序不應該試圖去修改它。



  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved