C语言中的restrict,const,volatile

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_t n);
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 gcc 4.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因为程序不应该试图去修改它。



你可能感兴趣的:(C语言中的restrict,const,volatile)