上下文
以GCC优化命名的“严格别名”是编译器假设内存中的值不会通过类型的左值(“声明的类型”)访问,该值与写入的值的类型非常不同( “有效型”) . 如果必须考虑到写入指向 float
的指针可能会修改 int
类型的全局变量,则此假设允许代码转换不正确 .
GCC和Clang都在a standard description full of dark corners中提取了最多的含义,并且在实践中对生成的代码的性能存在偏差,假设指向 struct thing
的第一个成员的 int
指针没有别名指向 int
的第一个成员的 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这个函数总是返回1,也就是说 p
和 q
不能是同一个内存位置的别名:
e:
movl $1, (%rdi)
movl $1, %eax
movl $2, (%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 $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方法的一个论点是this message,它是一个长线程的一部分,它也讨论了联合在严格别名规则中的作用 .
问题
但是,我怀疑以下示例,其中没有 struct
:
int g(int (*p)[10], int (*q)[10]) {
(*p)[3] = 1;
(*q)[4] = 2;
return (*p)[3];
}
GCC版本4.4.7通过当前版本7快照在Matt Godbolt的有用网站上优化函数 g
,好像 (*p)[3]
和 (*q)[4]
无法别名(或者更确切地说,好像程序已调用UB,如果他们这样做):
g:
movl $1, 12(%rdi)
movl $1, %eax
movl $2, 16(%rsi)
ret
是否有任何标准读数证明这种非常严格的严格混叠方法是正确的?如果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
错过优化 .
2 回答
在:
*p
和*q
是数组类型的左值;如果它们可能重叠,则对它们的访问受第6.5节第7段(所谓的"strict aliasing rule")的管辖 . 但是,由于它们的类型相同,因此不会对此代码造成问题 . 然而,对于对该问题给出全面答案所需的一些相关问题,该标准非常模糊,例如:(*p)
和(*q)
实际上需要"access"(因为术语在6.5p7中使用)到它们所指向的数组?如果他们不这样做,那么很容易认为表达式(*p)[3]
和(*q)[4]
本质上降级为指针算术和两个int *
的解除引用,这两个可以清楚地别名 . (这不是一个完全不合理的立场; 6.5.2.1 Array Subscripting 说其中一个表达式应该有''指向完整对象类型的'',另一个表达式应该有整数类型,结果类型为''type'' - 所以根据通常的转换规则,数组左值必然降级为指针;唯一的问题是在转换发生之前是否访问了数组 .但是,为了捍卫
(*p)[3]
纯粹等同于*((int *)p + 3)
的观点,我们必须证明(*p)[3]
不需要评估(*p)
,或者如果确实如此,则访问没有未定义的行为(或定义但不需要的行为) . 我不认为标准的准确措辞有任何理由允许(*p)
未被评估;这意味着如果定义了(*p)[3]
的行为,则表达式(*p)
必须不具有未定义的行为 . 所以,问题实际上归结为*p
和*q
是否已经定义了行为,如果它们引用了相同类型的部分重叠数组,实际上它们是否可能同时这样做 .对于
*
运算符的定义,标准说:*p
和*q
不能重叠 - 因为 Build 任何一个对象都会使另一个对象无效 - 因此(*p)[3]
和(*q)[4]
不能别名 .问题是对这些问题没有适当的指导 . 在我看来,应采取保守的方法:不要认为这种混叠是合法的 .
特别是,6.5中的"effective type"措辞提出了一种可以 Build 特定类型的对象的方法 . 这似乎是一个很好的选择,这是明确的;也就是说,您不能通过设置其有效类型(包括通过具有声明类型的方式)来 Build 对象,并且限制其他类型的访问;此外, Build 一个对象取消 Build 任何现有的重叠对象(要清楚,这是外推,而不是实际的措辞) . 因此,如果
(*p)[3]
和(*q)[4]
可能是别名,则p
或q
不指向对象,因此*p
或*q
之一具有未定义的行为 .恕我直言,该标准不允许确定大小的数组同时重叠(*) . n1570草案在6.2.7兼容类型和复合类型(强调我的)中说:
由于对象的存储值只能通过具有兼容类型的左值表达式(6.5表达式§7的简化读取)访问,因此不能对不同大小的数组进行别名,也不能使具有相同大小的数组重叠 . 因此,在函数g中,p和q应该指向相同的数组或非重叠的数组,这允许优化 .
对于函数f和k,我的理解是,根据标准允许优化,但开发人员尚未实现 . 我们必须记住,只要其中一个参数是一个简单的指针,就可以指向另一个数组的任何元素,并且不会发生任何优化 . 所以我认为缺乏优化只是UB着名规则的一个例子:任何事情都可能发生, including the expected result .