__restrict
__restrict
restrict,别名分析,编译器优化,未定义行为。
编译器面对两个指针时,默认必须假设它们可能指向同一块内存,这个假设叫别名(aliasing)。一条对 *a 的写可能同时改掉 *b 的值,所以在证明 a、b 不重叠之前,编译器不能把读提前、不能把写合并,循环也很难向量化。restrict 干的事就是让程序员出面做一个承诺:这几块内存互不重叠,你尽管优化。
别名让编译器处处设防
cppreference 的 restrict 页面给过一组最小对比,同一段函数加不加 restrict,在 \(64\) 位 Intel 平台上编出来的汇编不一样[1] :
int foo(int *a, int *b) {
*a = 5;
*b = 6;
return *a + *b;
}
int rfoo(int *restrict a, int *restrict b) {
*a = 5;
*b = 6;
return *a + *b;
}
foo 的汇编里,编译器回读了一次 *a:
foo:
movl $5, (%rdi) ; 把 5 存进 *a
movl $6, (%rsi) ; 把 6 存进 *b
movl (%rdi), %eax ; 回读 *a,因为上面的写可能已经改过它
addl $6, %eax ; 再加上 6
ret
rfoo 的汇编直接把返回值算成了编译期常量:
rfoo:
movl $11, %eax ; 结果是 11,编译期就算好了
movl $5, (%rdi) ; 把 5 存进 *a
movl $6, (%rsi) ; 把 6 存进 *b
ret
差别在回读那一步。foo 里 *b = 6 可能恰好写到 \(a\) 指向的位置——调用方传 foo(&x, &x) 完全合法——所以 *a + *b 不一定是 \(11\),编译器只能老老实实把 *a 从内存里读回来再加。rfoo 里 restrict 承诺 \(a\)、\(b\) 各指一段独占的内存,*b = 6 动不到 *a,于是返回值确定是 \(5 + 6 = 11\),直接折叠成常量。两个 store 仍然保留,因为它们是函数对外可见的副作用,不能省。
我用 CLion 自带的 GCC 13.1.0(MinGW-w64)在 -std=c99 -O2 下把这两个函数重新编译了一遍,优化结论和上面完全一致:foo 回读 *a,rfoo 把返回值算成 \(11\)。唯一的差别在寄存器名——本机是 Windows x64 调用约定,前两个参数走 rcx、rdx,而 cppreference 的示例是 Linux 的 System V 约定,用 rdi、rsi,两边的指令序列一一对应。
restrict 承诺的是独占访问
restrict 是 C99 引入的类型限定符(type qualifier),和 const、volatile 并列,只能加在指向对象类型的指针上。它承诺的内容写在标准里,大意是:在声明 restrict 指针 \(P\) 的块每次执行期间,如果有某个对象可以通过 \(P\)(直接或间接)访问到,并且被以任何方式修改了,那么该块内对这个对象的所有访问(读和写)都必须通过 \(P\) 进行,否则行为未定义[2] 。
cppreference 的例子把这个边界说得很清楚:
void f(int n, int * restrict p, int * restrict q) {
while (n-- > 0)
*p++ = *q++; // 通过 *p 修改的对象,和通过 *q 读的对象没有重叠
}
void g(void) {
extern int d[100];
f(50, d + 50, d); // 合法,两段区间不重叠
f(50, d + 1, d); // 未定义行为,d[1] 同时被 p 和 q 访问
}
两个关键点。一是"被修改"是前提:如果对象从头到尾只被读、从没被写,那它允许被多个 restrict 指针别名着访问,因为只读访问不会破坏任何优化假设。二是这个承诺以块为单位,函数参数这种最常见的用法,块就是整个函数体每次执行,所以 restrict 参数承诺的是"这次调用期间这块内存归我独占"。
违反承诺的代价是未定义行为
restrict 不会让编译器报警,写错了就是静默的未定义行为,最坏的情况是优化把结果算错,而不是崩给你看。上面 f(50, d + 1, d) 就是典型:参数类型完全匹配,编译器照单全收,只有跑起来才可能出问题。
restrict 指针之间的赋值也受约束:把一个 restrict 指针赋给另一个 restrict 指针,本身就是未定义行为,只有几条例外——从外层块赋给内层块、把 restrict 指针当实参传给 restrict 形参、以及 return 出去[3] 。反过来,把 restrict 指针赋给普通指针是自由的,编译器能分析清楚的话优化照样保留。
还有一点容易被忽略:restrict 对编译器只是提示,不是命令。删掉程序里所有的 restrict,符合规范的程序可观察行为不变;编译器也可以完全无视它,照样按保守方式编。所以它不改变程序语义,只改变编译器敢不敢优化。
C 有 restrict,C++ 只有下划线拼写
C99 起 restrict 是标准关键字。C++ 从来没有把它收进标准,restrict 在 C++ 里不是关键字——GCC 的文档写得很直接,因为没法用 -std=c99 编译 C++,所以 restrict 不是 C++ 的关键字[4] 。于是各家编译器用下划线拼写当扩展:
- GCC 和 Clang 用
__restrict__或__restrict,语义对齐 C99 的restrict。 - MSVC 用
__restrict,_restrict是它的同义词,除非开了/Za关扩展。
所以同样的代码在 C 里写 int *restrict a,到了 C++ 得写 int *__restrict a。这层拼写差异是跨语言时最容易踩的地方,尤其是 C 头文件直接给 C++ 用的时候。
MSVC 的 __restrict 更弱
MSVC 的 __restrict 和 C99 的 restrict 不是一回事。官方文档列出的差异里,最关键的一条是它不传播 no-alias 属性:把一个 __restrict 变量赋给普通变量之后,编译器仍然允许那个普通变量被别名,这一点和 C99 的 restrict 关键字行为不同[5] 。换句话说,MSVC 的版本更像一个只挂在那个变量身上的局部标记,而 C99 的 restrict 是参与类型系统、以块为作用域的限定符。
MSVC 还区分了两种写法:__restrict 只能加在变量上,__declspec(restrict) 只能加在函数声明和定义上,想让整个函数的别名行为都变掉就用后者。从 Visual Studio 2015 开始,__restrict 可以加在 C++ 引用上。它和 volatile 同时出现时,volatile 优先。
GCC 额外支持引用和 this
GCC 在 C99 语义之上多给了两样东西。一是受限引用,int &__restrict__ rref 表示这个引用在局部上下文中不别名;二是成员函数限定符,把 __restrict__ 加在成员函数后面,函数体内的 this 就等效于 T *__restrict__ const this,等于承诺这次调用里这个对象不被别的指针碰[6] 。后者的解释和 const、volatile 限定符不同——const 作用在对象上,__restrict__ 作用在指针 this 上。
什么时候值得标
收益最实在的地方是函数参数,尤其是那种参数里带着一进一出两个数组、循环里一边读一边写的中段代码,标了 restrict 编译器才敢向量化、才敢省掉回读。cppreference 列的用法模式里,函数参数是"最流行"的一种,另外两种是块级局部指针和文件作用域指针:局部指针能只对紧循环这一段做断言,宏展开的时候尤其有用;文件作用域指针适合指向动态分配的全局数组,让编译器能像访问静态数组那样优化它[7] 。
代价也很明确:写错了没有任何提示,只有未定义行为;而且不同编译器的下划线拼写不通用,MSVC 的语义还比 C99 弱一档。所以它只该用在你能确认独占的地方——两个数组确实不重叠、这个指针确实没人同时碰。拿不准就宁可多写一层 memcpy 到临时缓冲区,也比赌一个 UB 安全。
C++ 标准到现在都没收 restrict,委员会讨论过但始终没落地,这也是各家只能各搞一套下划线扩展的原因。真要在 C++ 里用,先定好目标编译器,别把 MSVC 的 __restrict 当成 C99 的 restrict 来期待。

浙公网安备 33010602011771号