C restrict 限定符是否传递?

Is the C restrict qualifier transitive?

虽然有许多示例 [1][2][3] 说明了 restrict 关键字的工作原理,但我不完全确定受限关系在它可以指向的指针上是否具有传递性。例如,以下代码声明了一个结构,其中包含一个整数和一个指向整数的指针。

typedef struct container_s {
  int x;
  int *i;
} container_s;


int bar(container_s *c, int *i) {
  int* tmp = c->i;
  *tmp = 5;      
  *i = 4;
  return *tmp;
}

int main(){
  return 0;
}

编译器是否需要额外的load指令来最后访问tmp(返回值),因为它无法推断*i*tmp不别名?

如果是这样,这个新定义是否会修复该负载?

int bar(container_s *c, int* restrict i) { ... }

编辑

这种情况 int bar(container_s *c, int * restrict i) { ... } 在我生成 LLVM IR (clang -S -O3 -emit-llvm) 时删除了提取负载。但是,我不明白为什么接下来的两个修改在以下情况下没有删除最终负载:

  1. 我将函数的定义更新为:

    int bar(container_s * restrict c, int *i) { ... }
    
  2. 我更新结构如下(为什么编译器不能推断不需要额外加载?):

    typedef struct container_s {
      int x;
      int * restrict i;
    } container_s;
    
    int bar(container_s *c, int *i) { ... }
    

"would this new header fix that load?", --> 否。restrict 引用 i,并访问其字段:

... requires that all accesses to that object use, directly or indirectly, the value of that particular pointer... C11 §6.7.3 8

但当这些字段又用于访​​问其他数据时,不会扩展以限定这些字段。

#include<stdio.h>

typedef struct container_s {
  int x;
  int *i;
} container_s;


int bar(container_s * c, int* restrict  i) {
  int* tmp = c->i;
  *tmp = 5;
  *i = 4;
  return *tmp;
}

int main(void) {
  int i = 42;
  container_s s = { 1, &i };

  printf("%d\n", bar(&s, &i));
  printf("%d\n", i);
  printf("%d\n", *(s.i));
}

输出

4
4  
4

我无法理解这里如何应用传递性,但我可以谈谈你的例子。

Does the compiler need an extra load instruction for the last access of tmp (the returned value) because it cannot infer that *i and *tmp do not alias?

编译器确实无法安全地推断出 *i*tmp 不会在您的原始代码中使用别名,正如您恰当地证明的那样。这并不意味着编译器特别需要发出 * 运算符的抽象机器语义隐含的 load 指令,但它确实需要注意处理别名问题 不知何故.


If so, would [restrict-qualifying parameter i] fix that load?

在函数定义中向参数i添加restrict限定条件,对程序的行为提出以下附加要求(源自C2011,6.7.3.1/4的文本):在 bar() 的每次执行期间,因为 i 是(平凡地) 基于 i,并且 *i 用于访问对象它指定,并且指定的对象在 bar() 的执行期间被修改(至少通过 *i),用于访问由 *i 指定的对象的每个其他左值也应基于其地址在 i.

*tmp 被访问,其地址 tmp 不是基于 i。因此,如果 i == tmp(即,如果在某些调用中 i == c->i),则程序无法符合。在这种情况下,它的行为是未定义的。编译器可以自由地发出假定程序符合的代码,因此特别是,在 restrict 限定的情况下,它可以发出假定语句

  *i = 4;

不修改*tmp,即声明

  *tmp = 5;

不修改*i。事实上,这似乎与 restrict 的定义和表达的意图一致,即编译器可以自由地精确地做出这些假设。

特别是,如果编译器选择通过执行 *tmp 的可能冗余加载来处理原始代码中出现别名的可能性,那么在符合 restrict 的版本中它可能会选择通过省略 load 来优化。但是,生成的机器代码决 不需要 在两种情况之间有任何区别。也就是说,一般来说,您不能依赖编译器来利用它可用的所有优化。

更新:

后续问题询问为什么clang在特定情况下不执行特定优化。首先,必须重申,C 编译器不负责对给定源代码执行任何可能的特定优化,除非它们自己记录。因此,人们通常不能从未执行给定优化的事实中得出任何结论,而且询问为什么未执行给定优化也很少有用。

关于您所能做的——我正在从这个角度解释这些问题——是问所讨论的优化是否是符合标准的编译器可以拥有的 执行。在这种情况下,标准强调通过采取不寻常的步骤来澄清 restrict 不会对实现施加优化义务:

A translator is free to ignore any or all aliasing implications of uses of restrict.

(C2011, 6.7.3.1/6)

话虽如此,继续提问。

  1. 在此代码变体中,*tmp 是一个左值,其地址基于 restrict 限定指针 c。它指定的对象在函数范围内通过左值访问,并在该范围内修改(通过 *tmp,因此编译器当然可以看到它)。 *i 的地址不是基于 c,因此编译器可以自由假设 *i 不会别名 *tmp,就像在原始问题中一样。

  2. 这种情况不同。尽管允许限制限定结构成员,但 restrict 只有在限定 普通标识符 (C2011, 6.7.3.1/1) 时才有效,其中结构成员名称是不是(C2011,6.2.3)。在这种情况下,restrict 没有效果,并且为了确保符合行为,编译器必须考虑 c->i*i(以及 *tmp)是别名的可能性。