同一类型数组之间的GCC和严格别名 上下文

同一类型数组之间的GCC和严格别名 上下文,c,gcc,strict-aliasing,C,Gcc,Strict Aliasing,“严格别名”(以GCC优化命名)是编译器的一种假设,即内存中的值不会通过类型(“声明的类型”)的左值访问,该类型与写入值时使用的类型(“有效类型”)非常不同。如果必须考虑到写入指向float的指针可能会修改int类型的全局变量,则此假设允许进行不正确的代码转换 GCC和Clang都从中提取了最大的意义,并且在实践中对生成的代码的性能有偏见,它们假设指向int结构对象的int第一个成员的指针不会别名指向struct对象的int第一个成员的指针: struct thing { int a; }; s

“严格别名”(以GCC优化命名)是编译器的一种假设,即内存中的值不会通过类型(“声明的类型”)的左值访问,该类型与写入值时使用的类型(“有效类型”)非常不同。如果必须考虑到写入指向
float
的指针可能会修改
int
类型的全局变量,则此假设允许进行不正确的代码转换

GCC和Clang都从中提取了最大的意义,并且在实践中对生成的代码的性能有偏见,它们假设指向
int
结构对象的
int
第一个成员的指针不会别名指向
struct对象的
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;
}
int g(int (*p)[10], int (*q)[10]) {
  (*p)[3] = 1;
  (*q)[4] = 2;
  return (*p)[3];
}
GCC和Clang都表示函数始终返回1,即
p
q
不能是同一内存位置的别名:

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

GCC优化了上述函数
h

h:
        movl    $1, 12(%rdi)
        movl    $1, %eax
        movl    $2, 8(%rsi)
        ret
到目前为止,只要你认为
p->a
p->t[3]
以某种方式访问一个整体
struct thing
(分别是
struct arr
),就有可能认为使用位置别名会违反6.5:6-7中的规则。这是GCC方法的一个论点是,这是一个长线程的一部分,该线程还讨论了联合在严格别名规则中的作用

问题: 然而,我对下面的例子有疑问,在这个例子中没有
struct

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;
}
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一样):

是否有任何对标准的解读可以证明这种严格的别名处理方法是合理的?如果GCC在这里的优化是合理的,那么参数是否也适用于GCC未优化的函数
f
k
的优化

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
k
的遗漏优化的情况下,标准不允许一定大小的数组同时重叠(*)。草案n1570在6.2.7中规定了兼容型和复合型(强调型):

§2所有涉及相同对象或功能的声明应具有兼容类型; 否则,行为是未定义的

§3复合类型可由两种兼容类型构成;这是一种 与这两种类型兼容,并满足以下条件:

  • 如果两种类型都是数组类型,则应用以下规则:
    • 如果一种类型是已知常量大小的数组,则复合类型是 那种尺寸
由于对象的存储值只能由具有兼容类型的左值表达式(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段(所谓的“严格别名规则”)的管辖。但是,由于它们的类型相同,因此这段代码不存在问题。然而,该标准对于全面回答该问题所需的一些相关问题非常模糊,例如:

  • (*p)
    (*q)
    是否确实需要“访问”(如6.5p7中使用的术语)它们所指向的阵列?如果他们不这样做,那么很有可能会认为表达式
    (*p)[3]
    (*q)[4]
    本质上退化为指针算术和两个
    int*
    的解引用,这两个表达式显然可以使用别名。(这不是一个完全不合理的观点;6.5.2.1数组下标表示其中一个表达式的类型应为“指向完整对象类型的指针”,另一个表达式的类型应为整数类型,结果的类型为“类型”-因此数组左值必须按照通常的转换规则;唯一的问题是数组是否在转换发生之前被访问)

  • 然而,为了捍卫
    (*p)[3]
    完全等同于
    *((int*)p+3)
    的观点,我们必须证明
    (*p)[3]
    不需要对
    (*p)
    进行评估,或者如果需要,访问没有未定义的行为(或定义但不需要的行为)。我不相信标准的精确措辞中有任何理由允许不评估
    (*p)
    ;这意味着