如何理解C语言关键字restrict?

对教材上的解释不是很理解。这个关键字的作用是什么,以及在什么情况下使用?
关注者
305
被浏览
11503

2 个回答

要理解 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

如果我们确保两个指针不指向同一数据,就可以用 restrict 修饰指针类型:
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最多三选二。
为什么?