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,即 p
和 q
不能是同一内存位置的别名:
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->a
或 p->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 未优化的函数 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
和 [= 的错过优化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]
可以别名,则 p
或 q
不指向对象,因此 *p
或 [=12] 之一=] 有未定义的行为。
上下文
“严格别名”,以 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,即 p
和 q
不能是同一内存位置的别名:
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->a
或 p->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 未优化的函数 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
和 [= 的错过优化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]
可以别名,则 p
或 q
不指向对象,因此 *p
或 [=12] 之一=] 有未定义的行为。