为何bar函数内的类型双关会影响基于严格别名的编译器优化?
示例1:foo函数
int foo(int *a, long *b) { int t = *a; *b = 0; // 不会改变*a的值 return *a - t; // 可直接优化为0 }
由于
a和b是指向不兼容类型的指针,C和C++规定对象只能通过兼容类型的左值访问其存储值,因此对*b的赋值不会影响存在寄存器中t变量里的*a值,减法结果必然为0,编译器可以做折叠优化。
示例2:bar函数
int bar(int *a, long *b) { int t = *a; for (int i = 0; i != sizeof *b; ++i) ((unsigned char*)b)[i] = 0; return *a - t; // 不能做折叠优化 }
这种情况下编译器无法折叠返回表达式,因为调用函数时传入
b == a是合法的。但如果给a和b加上restrict关键字,传入重叠对象的调用就会变成非法,编译器就能重新进行优化。
问题
我明白unsigned char*和char*一样可以用来做别名操作,但我觉得这两个函数的参数类型已经明确了——a是int*,b是long*,这就意味着它们不会互为别名。我的认知是:函数参数受严格别名规则约束,不管是以下哪种情况:
a和b指向重叠内存(比如通过reinterpret_cast转换),这属于未定义行为a和b指向不重叠的内存
像返回语句折叠这种类型感知优化都应该对foo和bar同样生效。那为什么bar函数里用了类型双关,就改变了编译器能不能做折叠优化的结果?
解答
问题的关键在于严格别名规则的特殊例外:C/C++标准明确允许unsigned char*(以及char*、signed char*)别名任何类型的对象,这是完全合法的操作,不属于未定义行为。
在foo函数中,我们直接通过long*类型的b去赋值*b:如果a和b指向同一块内存(也就是用long*访问原本是int类型的对象),这就违反了严格别名规则,属于未定义行为。编译器可以基于“这种违规场景不存在”的假设进行优化,认定*b = 0不会影响*a的值,所以能把*a - t直接折叠成0。
但在bar函数中,我们是通过unsigned char*来逐字节修改b指向的内存:哪怕b和a指向同一块内存(原本是int类型的对象),用unsigned char*去访问和修改也是完全合法的。这时候编译器必须考虑“b和a指向同一块内存,修改b的字节会改变*a的值”这种合法场景,所以不能把返回表达式折叠成0——因为此时*a可能已经被修改,和t不再相等。
总结一下:
foo中如果出现指针重叠,属于未定义行为,编译器可以无视这种情况bar中如果出现指针重叠,是合法行为,编译器必须处理这种情况
这就是两个函数优化结果不同的根本原因。
内容的提问来源于stack exchange,提问作者nhnl

