Dan*_*sig 22 c++ for-loop specifications extern language-lawyer
在调试一些遗留代码时,我偶然发现令人惊讶的(对我而言)编译器行为.现在我想知道C++规范中的任何子句是否允许以下优化,其中函数调用对for-condition的副作用被忽略:
void bar() 
{
   extern int upper_bound;
   upper_bound--;
}
void foo()
{
   extern int upper_bound; // from some other translation unit, initially ~ 10
   for (int i = 0; i < upper_bound; ) {
      bar();
   }
}
在得到的解析中,存在一个控制路径,其中upper_bound保存在寄存器中,并且upper_boundin 的递减bar()永远不会生效.
我的编译器是Microsoft Visual C++ 11.00.60610.1.
老实说,我在N3242的 6.5.3和6.5.1中没有看到太多的摆动空间,但我想确定我没有错过任何明显的东西.
小智 11
该标准明确无误地阐明了两个声明upper_bound引用同一个对象.
3.5计划和联系[basic.link]
9两个相同的名称(第3条)和在不同范围内声明的名称应表示相同的变量,函数,类型,枚举器,模板或名称空间
- 两个名称都有外部链接,否则两个名称都有内部联系,并在同一个翻译单元中声明; 和
- 两个名称都指向同一名称空间的成员或同一类的成员,而不是继承.和
- 当两个名称都表示函数时,函数的参数类型列表(8.3.5)是相同的; 和
- 当两个名称都表示功能模板时,签名(14.5.6.1)是相同的.
这两个名字都有外部联系.两个名称都引用全局命名空间中的成员.两个名称都不表示函数或函数模板.因此,两个名称都指向同一个对象.建议你有单独的声明使这些基本事实无效就像是说int i = 0; int &j = i; j = 1; return i;可能会返回零,因为编译器可能已经忘记了j所指的内容.当然必须返回1.这必须工作,简单明了.如果没有,你发现了编译器错误.
Mik*_*son -2
如果您深入研究一下标准,这种行为似乎是正确的。
第一个提示位于第 3.3.1/4 节的注释中,其中表示:
局部 extern 声明 (3.5) 可以将名称引入到声明出现的声明区域中,也可以将名称(可能不可见)引入到封闭的命名空间中;
这有点模糊,似乎暗示编译器在传递函数时不需要upper_bound在全局上下文中引入名称bar(),因此,当upper_bound出现在foo()函数中时,这两个外部变量之间没有任何联系,并且因此,bar()据编译器所知,没有副作用,因此优化变成无限循环(除非 upper_bound 一开始就为零)。
但这种模糊的语言还不够,它只是一个警告性的说明,而不是正式的要求。
幸运的是,稍后在第 3.5/7 节中有一个精度,如下所示:
当没有发现具有链接的实体的块作用域声明引用某些其他声明时,则该实体是最内部封闭命名空间的成员。然而,这样的声明不会在其命名空间范围中引入成员名称。
他们甚至提供了一个例子:
namespace X {
  void p() {
    q();              // error: q not yet declared
    extern void q();  // q is a member of namespace X
  }
  void middle() {
    q();              // error: q not yet declared
  }
}
这直接适用于您给出的示例。
所以,问题的核心是编译器不需要在第upper_bound一个声明(在 bar 中)和第二个声明(在 foo 中)之间建立关联。
upper_bound因此,让我们检查一下假设两个声明未连接的优化含义。编译器这样理解代码:
void bar() 
{
   extern int upper_bound_1;
   upper_bound_1--;
}
void foo()
{
   extern int upper_bound_2;
   for (int i = 0; i < upper_bound_2; ) {
      bar();
   }
}
由于 bar 的函数内联,结果如下:
void foo()
{
   extern int upper_bound_1;
   extern int upper_bound_2;
   while( 0 < upper_bound_2 ) {
      upper_bound_1--;
   }
}
这显然是一个无限循环(据编译器所知),即使被upper_bound声明volatile,它也只会有一个未定义的终止点(每当upper_bound外部碰巧被设置为0或更少时)。upper_bound_1由于溢出,无限次(或无限次)递减变量 ( ) 具有未定义的行为。因此,编译器可以选择不执行任何操作,这显然是未定义行为时允许的行为。因此,代码变为:
void foo()
{
   extern int upper_bound_2;
   while( 0 < upper_bound_2 ) { };
}
这正是您在 GCC 4.8.2 生成的函数的汇编列表中看到的内容(带有-O3):
    .globl  _Z3foov
    .type   _Z3foov, @function
_Z3foov:
.LFB1:
   .cfi_startproc
    movl    upper_bound(%rip), %eax
    testl   %eax, %eax
    jle .L6
.L5:
    jmp .L5
    .p2align 4,,10
    .p2align 3
.L6:
    rep ret
    .cfi_endproc
.LFE1:
    .size   _Z3foov, .-_Z3foov
这可以通过添加 extern 变量的全局范围声明来修复,如下所示:
extern int upper_bound;
void bar() 
{
   extern int upper_bound;
   upper_bound--;
}
void foo()
{
   extern int upper_bound;
   for (int i = 0; i < upper_bound; ) {
      bar();
   }
}
产生这个程序集:
_Z3foov:
.LFB1:
    .cfi_startproc
    movl    upper_bound(%rip), %eax
    testl   %eax, %eax
    jle .L2
    movl    $0, upper_bound(%rip)
.L2:
    rep ret
    .cfi_endproc
.LFE1:
    .size   _Z3foov, .-_Z3foov
这是预期的行为,即,可观察的行为foo()相当于:
void foo()
{
   extern int upper_bound;
   upper_bound = 0;
}