【发布时间】:2015-05-22 18:07:34
【问题描述】:
假设我们有以下代码:
typedef struct {
int f1;
int f2;
} t_str;
int f(t_str* p, t_str* q)
{
p[0].f1++;
q[0].f2++;
p[0].f1++;
q[0].f2++;
p[0].f1++;
q[0].f2++;
p[0].f1++;
q[0].f2++;
p[0].f1++;
q[0].f2++;
p[0].f1++;
q[0].f2++;
p[0].f1++;
q[0].f2++;
p[0].f1++;
q[0].f2++;
p[0].f1++;
q[0].f2++;
return 0;
}
当我们用 -O3 选项编译它(我使用 gcc-5.1.0)时,编译器得到以下汇编程序:
f:
.LFB0:
.cfi_startproc
movl 8(%esp), %edx
movl 4(%esp), %ecx
movl 4(%edx), %eax
addl $9, (%ecx)
addl $9, %eax
movl %eax, 4(%edx)
xorl %eax, %eax
ret
.cfi_endproc
这意味着 gcc 决定访问 p 的字段 f1 和访问 q 的字段 f2 从不别名。我猜这来自假设相同类型的两个对象永远不会重叠或者它们是相同的。但我在标准中没有发现问题。
请问,任何人都可以在标准中找到这个问题,或者为什么 gcc 限制字段访问,或者评论发生了什么?
更新:
嗯,我也想到了第 6.5 节的第 7 段,但对我来说,对所有对象都有这样的明确形式会更舒服:
6.5.16.1 简单赋值
3 如果存储在一个对象中的值是从另一个对象中读取的 以任何方式与第一个对象的存储重叠,然后 重叠应准确且两个对象应具有合格或 兼容类型的不合格版本;否则,行为是 未定义。
很遗憾,这里不能使用这条规则。
现在看看,如果上面的代码我做了以下函数:
void main()
{
char * c = malloc(12);
memset(c, 0, 12);
f((t_str *)(c + 4), (t_str *)c);
printf("%d %d %d\n", ((t_str *)c)->f1, ((t_str *)c)->f2, ((t_str *)(c + 4))->f2);
}
现在我在执行过程中得到以下信息:
$ gcc-5.1.0 test1.c -O3 && ./a.out
0 9 0
$ gcc-5.1.0 test1.c -O0 && ./a.out
0 18 0
那么您认为这段代码的有效性如何?因为我不确定它是否符合第 6.5 节第 7 段。
PS:有趣的事情:
$ gcc-5.1.0 test1.c -O3 -fwhole-program && ./a.out
0 10 0
$ gcc-5.1.0 test1.c -O3 -flto && ./a.out
0 10 0
【问题讨论】:
-
我不确定我是否理解您的问题。 “别名”和“重叠”是什么意思?编译器所做的似乎是正确的,只要 p 和 q 在访问之间不改变(例如,通过处理中断等)。我认为您可以使用关键字“volatile”来强制编译器假设它们可能已经更改。
-
为了更有趣,看看如果您声明
int*pf1=&p->f1;int*pf2=&p->f2;等然后现在将p->f1++替换为(*pf1)++会发生什么。 -
alexanius 想知道标准是否明确排除了
q指向p->f2的可能性。由于t_str结构只有2 个int成员,因此可以将2 个此类结构的数组视为在地址&p->f2处包含第三个此类结构。我假装它被排除在外,但是为typedef struct { int f[2]; } t_str;做类似的案例可能会更困难 -
@Marc Glisse:看看
gcc在这种情况下优化了什么会很有趣,但我认为生成相同的代码应该仍然是正确的,或者甚至在没有负载的情况下更紧凑的代码/通过%eax存储 -
clang在 32 位和 64 位 x86 代码中生成的完全相同的代码(ABI 差异除外)
标签: c gcc optimization alias