关于C标准中extern const的疑问:编译器优化的标准依据
示例代码
extern const int foo; extern void blah(void); int toto(void) { int x = foo; blah(); int y = foo; return x + y; }
编译命令与汇编结果
使用以下命令编译:
arm-linux-gnueabihf-gcc -std=c99 -O2 -fno-pic -S extern_const2.c
生成的汇编代码如下:
toto: @ args = 0, pretend = 0, frame = 0 @ frame_needed = 0, uses_anonymous_args = 0 movw r3, #:lower16:foo movt r3, #:upper16:foo push {r4, lr} ldr r4, [r3] bl blah lsls r0, r4, #1 pop {r4, pc}
观察到的优化行为
从汇编代码可以看到,foo仅被读取一次,后续通过左移1位(等价于乘以2)计算返回值。这表明GCC假设foo在调用blah()期间值保持不变,Clang也存在相同的优化行为。
标准条款疑问
在ISO/IEC 9899:2017(C17)标准中,未找到直接针对该假设的明确条款:
- §6.7.3-12解释了
extern volatile const变量的特性(值可被硬件修改,但程序不可赋值),但不适用于普通的extern const变量。 - §6.7.3-6规定程序不得向
const变量赋值,但不确定这是否意味着外部调用无法修改该变量。该条款原文:若试图通过非const限定类型的左值修改定义为const限定类型的对象,行为未定义
另外,示例中的foo是声明而非定义为const,标准§6.7-5对声明与定义的区别说明:
声明指定一组标识符的解释和属性。标识符的定义是指该标识符的声明满足:——对于对象,为其预留存储;
核心问题
C标准中哪部分内容支持编译器对extern const变量做出「值保持不变」的假设?
标准依据分析
编译器的这个优化行为,核心来自以下几部分标准条款的约束与语义:
const对象的不可修改性(§6.7.3-6的延伸)
虽然示例中foo是extern const声明,但根据标准的一致性要求,foo的定义必须与其声明的类型限定符兼容——也就是说,foo的定义必然是const int foo = ...;(或等价的const限定定义)。而§6.7.3-6明确规定,通过非const左值修改定义为const的对象属于未定义行为。这意味着,任何符合标准的程序都不能修改
foo的值,无论代码位于当前编译单元还是其他单元。如果有代码尝试修改foo,整个程序的行为就已经是未定义的,编译器无需考虑这种情况,可以安全假设foo的值不会被改变。声明与定义的类型兼容性(§6.2.7-1)
标准§6.2.7-1规定:「所有对同一对象或函数的声明应具有兼容类型」。对于extern const int foo;的声明,对应的定义必须是兼容类型,即带const限定的int对象。如果定义没有const,声明与定义的类型不兼容,程序违反约束,属于未定义行为。因此,编译器可以确定
foo的定义必然是const对象,进而推断没有合法代码能修改它,所以可以安全地将两次读取优化为一次。优化的合法性(§5.1.2.3-6)
标准§5.1.2.3-6允许编译器进行优化,只要优化后的程序与严格符合标准的程序行为一致。由于任何修改const对象的操作都是未定义行为,编译器无需考虑这种场景,因此将两次读取foo合并为一次是符合标准的优化。
内容的提问来源于stack exchange,提问作者David Monniaux

