GCC 和相同类型数组之间的严格别名

GCC and strict aliasing between arrays of a same type

上下文

“严格别名”,以 GCC 优化命名,是编译器假设内存中的值不会通过与类型非常不同的类型(“声明的类型”)的左值访问值是用(“有效类型”)写入的。如果必须考虑写入指向 float 的指针可能会修改 int.

类型的全局变量,则此假设允许代码转换是不正确的

GCC 和 Clang 都从 a standard description full of dark corners 中提取了大部分含义,并且在实践中对生成代码的性能有偏见,假设指向 int 第一个成员的指针 struct thing 不为指向 struct object:

int 第一个成员的指针设置别名
struct thing { int a; };
struct object { int a; };

int e(struct thing *p, struct object *q) {
  p->a = 1;
  q->a = 2;
  return p->a;
}

GCC 和 Clang infer 函数总是 return 1,即 pq 不能是同一内存位置的别名:

e:
        movl    , (%rdi)
        movl    , %eax
        movl    , (%rsi)
        ret

只要同意这种优化的推理,那么 p->t[3]q->t[2] 在以下代码段中也被假定为不相交的左值就不足为奇了(或者更确切地说,如果调用者使用别名,调用者会导致 UB:

struct arr { int t[10]; };

int h(struct arr *p, struct arr *q) {
  p->t[3] = 1;
  q->t[2] = 2;
  return p->t[3];
}

GCC优化了上述功能h:

h:
        movl    , 12(%rdi)
        movl    , %eax
        movl    , 8(%rsi)
        ret

到目前为止一切顺利,只要看到 p->ap->t[3] 以某种方式访问​​整个 struct thing(resp. struct arr),就可以争辩说,使位置别名会违反 6.5:6-7 中规定的规则。关于这是 GCC 方法的一个论点是 this message,这是一个长线程的一部分,该线程还讨论了联合在严格别名规则中的作用。

问题

然而,我对下面的例子有疑问,其中没有 struct:

int g(int (*p)[10], int (*q)[10]) {
  (*p)[3] = 1;
  (*q)[4] = 2;
  return (*p)[3];
}

GCC 版本 4.4.7 到 Matt Godbolt 有用网站上的当前版本 7 快照优化功能 g 就好像 (*p)[3](*q)[4] 不能别名(或者更确切地说,好像该程序调用了 UB,如果他们调用的话):

g:
        movl    , 12(%rdi)
        movl    , %eax
        movl    , 16(%rsi)
        ret

是否有任何标准读物证明这种非常严格的严格别名方法是合理的?如果 GCC 在这里的优化是合理的,那么这些论点是否也适用于 GCC 未优化的函数 fk 的优化?

int f(int (*p)[10], int (*q)[9]) {
  (*p)[3] = 1;
  (*q)[3] = 2;
  return (*p)[3];
}

int k(int (*p)[10], int (*q)[9]) {
  (*p)[3] = 1;
  (*q)[2] = 2;
  return (*p)[3];
}

我愿意与 GCC 开发人员一起解决这个问题,但我应该首先决定不报告函数 g 的正确性错误或 f 和 [= 的错过优化37=].

恕我直言,标准不允许确定大小的数组同时重叠(*)。 draft n1570 在 6.2.7 Compatible type and composite type (emphasis mine) 中说:

§2 All declarations that refer to the same object or function shall have compatible type; otherwise, the behavior is undefined.

§3 A composite type can be constructed from two types that are compatible; it is a type that is compatible with both of the two types and satisfies the following conditions:

  • If both types are array types, the following rules are applied:
    • If one type is an array of known constant size, the composite type is an array of that size.
      ...

由于对象的存储值只能由具有兼容类型的左值表达式访问(6.5 表达式§7 的简化阅读),所以不能为不同大小的数组取别名,也不能有相同的数组重叠的大小。因此,在函数 g 中,p 和 q 应该指向同一个数组或指向非重叠数组,这允许优化。

对于函数 f 和 k,我的理解是根据标准允许优化,但开发人员尚未实现。我们必须记住,只要其中一个参数是一个简单的指针,它就可以指向另一个数组的任何元素,并且不会发生优化。所以我认为没有优化只是UB著名规则的一个例子:任何事情都可能发生,包括预期结果

在:

int g(int (*p)[10], int (*q)[10]) {
  (*p)[3] = 1;
  (*q)[4] = 2;
  return (*p)[3];
}

*p*q是数组类型的左值;如果它们可能重叠,对它们的访问受第 6.5 节第 7 段(所谓的 "strict aliasing rule")的约束。但是,由于它们的类型相同,因此这段代码不会出现问题。然而,该标准对于全面回答此问题所需的一些相关问题非常模糊,例如:

  • (*p)(*q) 实际上需要 "access" (因为该术语在 6.5p7 中使用)它们指向的数组?如果他们不这样做,很容易认为表达式 (*p)[3](*q)[4] 本质上会退化为指针算术和两个 int * 的取消引用,这显然是别名。 (这不是一个完全不合理的观点;6.5.2.1 数组下标其中一个表达式的类型应为“指向完整对象类型的指针”,其他表达式应具有整数类型,并且结果具有类型 ''type'' - 因此按照通常的转换规则,数组左值必然退化为指针;唯一的问题是数组是否为 在转换发生之前访问)。

  • 然而,为了捍卫 (*p)[3] 完全等同于 *((int *)p + 3) 的观点,我们必须证明 (*p)[3] 不需要求值(*p),或者如果有,则访问没有未定义的行为(或已定义但不需要的行为)。我认为标准的精确措辞没有任何理由允许 (*p) 不被评估;这意味着如果定义了 (*p)[3] 的行为,则表达式 (*p) 不能有未定义的行为。因此,问题实际上归结为 *p*q 如果它们引用相同类型的部分重叠数组是否有定义的行为,以及它们是否有可能同时这样做。

对于*运算符的定义,标准是这样说的:

if it points to an object, the result is an lvalue designating the object

  • 这是否意味着指针必须指向对象的开头? (这似乎就是这个意思)。在访问对象之前是否必须以某种方式建立该对象(并且建立一个对象是否会破坏任何重叠的对象)?如果两者都是这种情况,*p*q 不能重叠 - 因为建立任何一个对象都会使另一个对象无效 - 因此 (*p)[3](*q)[4] 不能别名。

问题是对这些问题没有合适的指导。在我看来,应该采取保守的做法:不要假设这种别名是合法的。

特别是,6.5 中的"effective type" 措辞提出了一种可以建立特定类型对象的方法。这似乎是一个很好的赌注,这是为了确定;也就是说,除了设置其有效类型(包括通过它具有已声明的类型)之外,您不能建立对象,并且其他类型的访问受到限制;此外,建立一个对象会取消建立任何现有的重叠对象(要清楚,这是推断,而不是实际的措辞)。因此,如果 (*p)[3](*q)[4] 可以别名,则 pq 不指向对象,因此 *p 或 [=12] 之一=] 有未定义的行为。