restrict 类型限定符

来自cppreference.com
< c‎ | language

C 类型系统中每一个独立的类型在都有数个该类型的限定版本,对应 constvolatile 及对于指向对象指针的 restrict 限定符中的一个、两个或全部三个。此页面描述 restrict 限定符的效果。

仅有指向对象类型的指针及其(可能多维的)数组 (C23 起)能有 restrict 限定;具体而言,以下是错误的:

  • int restrict *p
  • float (* restrict f9)(void)

restrict 语义仅应用于左值表达式;例如到 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); // OK
    f(50, d + 1, d); // 未定义行为: d[1] 被 f 中的 p 和 q 一同访问
}

若对象决不被修改,则它可以被别名引用,并被异于 restrict 限定的指针访问(注意若对象为别名引用的 restrict 限定指针所指,则别名引用可能抑制优化)。

将一个 restrict 指针赋值给另一个是未定义行为,除非将指向外部块的指针赋值给内部块中的指针(包括在调用含 restrict 指针参数的函数时,以 restrict 指针为参数),或从函数返回指针(还有在前一个指针的块已经结束时):

int* restrict p1 = &a;
int* restrict p2 = &b;
p1 = p2; // 未定义行为

restrict 指针可以自由地赋值给非 restrict 指针,只要编译器还能优化代码,优化机会还是保留就位的:

void f(int n, float * restrict r, float * restrict s) {
   float * p = r, * q = s; // OK
   while(n-- > 0) *p++ = *q++; // 几乎肯定优化成仅如 *r++ = *s++ 一般
}

若以 restrict 类型限定符声明数组类型(通过使用 typedef ),则数组类型无 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 前 OK ; 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); // OK
   f(20, n, p, p+10); // 可能是未定义行为(取决于 f 所为)
}

注解

restrict 限定符(像寄存器存储类)是有意使用以促进优化的。而从所有组成一致程序的预处理翻译单元中,删除所有此限定符的实例不会影响其含义(即可观的行为)。

编译器可以忽略任何一个或全部使用 restrict 的别名使用暗示。

欲避免未定义行为,程序员应该确保 restrict 限定指针所做的别名引用断言不会违规。

许多编译器提供作为 restrict 对立面的语言扩展:指示即使指针类型不同,也可以别名使用的属性: may_alias (gcc)

使用模式

restrict 限定指针有几种常用的使用模式:

文件作用域

文件作用域的 restrict 限定指针必须在程序运行期间指向单个数组的元素。该数组对象不可以通过 restrict 指针和通过其声明名称(若有的话)或另一个 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 的入口处,必须提供 restrict 指针对关联数组的独占访问。特别是,在 fbc 都不可以指入 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); // OK
           f( 50,   d, d+50); // OK
           f( 99, d+1,    d); // 未定义行为
    c = d; f( 99, d+1,    e); // 未定义行为
           f( 99,   e,  d+1); // OK
}

注意将 c 指向 b 所关联的数组是允许的。还要注意,对于这些目的,关联到通常指针的“数组”,仅表示数组对象的那一部分实际上是由该指针引用的。

注意在上例中,编译器能推断 a 和 b 不会别名使用,因为 b 的常性确保这点不会变得依赖函数体。程序员能等价地写 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 {    // restrict 指针断言  
   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 时全部指向无交集的存储区。
   // ...
}

关键词

restrict

示例

代码生成样例;以 -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      # 将从 *a 读得的值加 6
    ret
 
rfoo:
    movl      $11, %eax   # 结果是 11,编译时常量
    movl      $5, (%rdi)  # 存储 5 于 *a
    movl      $6, (%rsi)  # 存储 6 于 *b
    ret

引用

  • C17 标准(ISO/IEC 9899:2018):
  • 6.7.3.1 Formal definition of restrict (第 89-90 页)
  • C11 标准(ISO/IEC 9899:2011):
  • 6.7.3.1 Formal definition of restrict (第 123-125 页)
  • C99 标准(ISO/IEC 9899:1999):
  • 6.7.3.1 Formal definition of restrict (第 110-112 页)