restrict type qualifier (since C99)
C语言
类型系统
中的每个独立类型都有若干该类型的
限定
版本,对应
const
、
volatile
以及(针对对象类型指针的)
restrict
限定符中的一种、两种或全部三种组合。本页描述
restrict
限定符的作用效果。
只有指向 对象类型 或其(可能多维的)数组 (C23起) 的指针才能被 restrict 限定;特别地,以下情况是 错误的 :
- int restrict * p
- float ( * restrict f9 ) ( void )
restrict 语义仅适用于左值表达式;例如,转换为 restrict 限定指针或返回 restrict 限定指针的函数调用均非左值,此时限定符不产生任何效果。
在声明了受限指针
P
的代码块(通常是
P
作为函数参数的函数体)的每次执行期间,如果通过
P
(直接或间接)可访问的某个对象被以任何方式修改,那么该代码块中对该对象的所有访问(包括读取和写入)都必须通过
P
(直接或间接)进行,否则行为是未定义的:
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] 在函数 f 中同时通过 p 和 q 被访问 }
如果对象从未被修改,它可能会被别名化并通过不同的 restrict 限定指针访问(请注意,若被别名化的 restrict 限定指针所指向的对象本身也是指针,这种别名化可能会抑制优化)。
从一个受限指针向另一个受限指针赋值是未定义行为,除非是从外层代码块的指针向内层代码块指针赋值(包括在调用带有受限指针参数的函数时使用受限指针实参),或从函数返回时(以及在其他情况下当源指针所在代码块已结束时):
int* restrict p1 = &a; int* restrict p2 = &b; p1 = p2; // 未定义行为
受限指针可以自由赋值给非受限指针,只要编译器能够分析代码,优化机会就仍然存在:
void f(int n, float * restrict r, float * restrict s) { float *p = r, *q = s; // 正确 while (n-- > 0) *p++ = *q++; // 几乎确定会像 *r++ = *s++ 一样被优化 }
|
若数组类型通过 typedef 使用 restrict 类型限定符声明,则数组类型本身不具有 restrict 限定,但其元素类型具有 restrict 限定: |
(C23 前) |
|
数组类型与其元素类型始终被视为具有相同的 restrict 限定: |
(C23 起) |
typedef int *array_t[10]; restrict array_t a; // a的类型是 int *restrict[10] // 注意:clang和icc基于array_t不是指针类型的原则拒绝此用法 void *unqual_ptr = &a; // C23之前有效;C23起报错 // 注意:clang即使在C89-C17模式下也采用C++/C23的规则
在函数声明中,关键字
restrict
可以出现在用于声明函数参数数组类型的方括号内。它限定数组类型转换后的指针类型:
void f(int m, int n, float a[restrict m][n], float b[restrict m][n]); void g12(int n, float (*p)[n]) { f(10, n, p, p+10); // 正确 f(20, n, p, p+10); // 可能出现未定义行为(取决于函数f的具体实现) }
目录 |
注释
restrict限定符(类似于register存储类)的预期用途是促进优化,从组成符合规范程序的所有预处理翻译单元中删除该限定符的所有实例不会改变其含义(即可观察行为)。
编译器可以自由忽略
restrict
使用带来的任何或所有别名影响。
为避免未定义行为,程序员必须确保由 restrict 限定指针所作的别名断言不被违反。
许多编译器作为语言扩展提供了与
restrict
相反的功能:一种指示指针可能产生别名的属性,即使它们的类型不同:
may_alias
(gcc),
使用模式
以下是几种常见的 restrict 限定指针使用模式:
文件作用域
文件作用域的 restrict 限定指针必须在程序整个生命周期内指向单个数组对象。该数组对象不能同时通过受限指针和其声明名称(如果存在)或其他受限指针进行引用。
文件作用域受限指针在提供对动态分配的全局数组的访问时非常有用;restrict语义使得通过该指针进行的引用优化能够达到与通过声明名称对静态数组进行引用同等的效果:
float *restrict a, *restrict b; float c[100]; int init(int n) { float * t = malloc(2*n*sizeof(float)); a = t; // a 指向前半部分 b = t + n; // b 指向后半部分 } // 编译器可通过 restrict 限定符推断出 // 名称 a、b 和 c 之间不存在潜在的别名冲突
函数参数
restrict限定指针最常见的用例是作为函数参数使用。
在以下示例中,编译器可推断被修改对象不存在别名化,从而对循环进行激进优化。
进入
f
时,受限指针 a 必须对其关联数组提供独占访问权限。特别地,在
f
函数内,
b
和
c
均不能指向与
a
关联的数组,因为二者均未基于
a
被赋值指针。对于
b
,这从其声明中的 const 限定符即可明确,但对于
c
,需要检查
f
的函数体:
float x[100]; float *c; void f(int n, float * restrict a, float * const b) { int i; for ( i=0; i<n; i++ ) a[i] = b[i] + c[i]; } void g3(void) { float d[100], e[100]; c = x; f(100, d, e); // 正确 f( 50, d, d+50); // 正确 f( 99, d+1, d); // 未定义行为 c = d; f( 99, d+1, e); // 未定义行为 f( 99, e, d+1); // 正确 }
注意,允许 c 指向与 b 关联的数组内部。同时请注意,就本条款而言,与特定指针关联的"数组"仅指通过该指针实际引用的数组对象的那部分。
请注意,在上面的示例中,编译器可以推断出 a 和 b 不存在别名引用,因为 b 的常量性保证了它在函数体内不会依赖于 a。同样地,程序员也可以编写 void f ( int n, float * a, float const * restrict b ) ,这种情况下编译器可以推断通过 b 引用的对象不会被修改,因此不会有被修改的对象同时通过 b 和 a 被引用。如果程序员编写 void f ( int n, float * restrict a, float * b ) ,编译器将无法在不检查函数体的情况下推断 a 和 b 的非别名关系。
通常来说,最好在函数原型中为所有非别名指针显式添加 restrict 标注。
块作用域
块作用域的 restrict 限定指针作出的别名断言仅限于其所在块。这使得局部断言仅适用于重要代码块(例如紧凑循环)成为可能。同时这也使得将接受 restrict 限定指针的函数转换为宏成为可能:
float x[100]; float *c; #define f3(N, A, B) \ do \ { int n = (N); \ float * restrict a = (A); \ float * const b = (B); \ int i; \ for ( i=0; i<n; i++ ) \ a[i] = b[i] + c[i]; \ } while(0)
结构体成员
限定为restrict的指针作为结构体成员时,其别名断言的作用域取决于访问该结构体时使用的标识符作用域。
即使该结构体在文件作用域中声明,当用于访问该结构体的标识符具有块作用域时,结构体中的别名断言也具有块作用域;这些别名断言仅在块执行或函数调用期间有效,具体取决于该结构体类型的对象是如何创建的:
struct t // 受限指针确保 { int n; // 成员指向互不重叠的存储空间 float * restrict p; float * restrict q; }; void ff(struct t r, struct t s) { struct t u; // r,s,u 具有块作用域 // r.p, r.q, s.p, s.q, u.p, u.q 在每次执行 ff 期间都应指向 // 互不重叠的存储空间 // ... }
关键词
示例
代码生成示例;使用 -S (gcc, clang 等) 或 /FA (visual studio) 编译
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; }
可能的输出:
; 在64位Intel平台上生成的代码: foo: movl $5, (%rdi) ; 将5存入*a movl $6, (%rsi) ; 将6存入*b movl (%rdi), %eax ; 重新从*a读取值(防止前次存储修改了该值) addl $6, %eax ; 将6与从*a读取的值相加 ret rfoo: movl $11, %eax ; 结果为编译时常量11 movl $5, (%rdi) ; 将5存入*a movl $6, (%rsi) ; 将6存入*b ret
参考文献
- C23 标准 (ISO/IEC 9899:2024):
-
- 6.7.3.1 restrict 的形式化定义 (p: TBD)
- C17 标准 (ISO/IEC 9899:2018):
-
- 6.7.3.1 restrict 的形式化定义 (p: 89-90)
- C11 标准 (ISO/IEC 9899:2011):
-
- 6.7.3.1 restrict 的形式化定义 (p: 123-125)
- C99标准(ISO/IEC 9899:1999):
-
- 6.7.3.1 restrict的形式化定义(第110-112页)