C++是否允许优化编译器忽略for-condition的副作用?

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();
   }
}
Run Code Online (Sandbox Code Playgroud)

在得到的解析中,存在一个控制路径,其中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.这必须工作,简单明了.如果没有,你发现了编译器错误.

  • 你正在混淆问题.问题出在编译阶段,并且是关于引入名称查找可见的名称的规则,并且您指的是关于如何将来自不同范围的声明的变量需要*链接*到相同实体的规则.而且,你的`i`和`j`的例子是关于内存别名规则,这与这个问题无关. (2认同)

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
  }
}
Run Code Online (Sandbox Code Playgroud)

这直接适用于您给出的示例。

所以,问题的核心是编译器不需要在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();
   }
}
Run Code Online (Sandbox Code Playgroud)

由于 bar 的函数内联,结果如下:

void foo()
{
   extern int upper_bound_1;
   extern int upper_bound_2;
   while( 0 < upper_bound_2 ) {
      upper_bound_1--;
   }
}
Run Code Online (Sandbox Code Playgroud)

这显然是一个无限循环(据编译器所知),即使被upper_bound声明volatile,它也只会有一个未定义的终止点(每当upper_bound外部碰巧被设置为0或更少时)。upper_bound_1由于溢出,无限次(或无限次)递减变量 ( ) 具有未定义的行为。因此,编译器可以选择不执行任何操作,这显然是未定义行为时允许的行为。因此,代码变为:

void foo()
{
   extern int upper_bound_2;
   while( 0 < upper_bound_2 ) { };
}
Run Code Online (Sandbox Code Playgroud)

这正是您在 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
Run Code Online (Sandbox Code Playgroud)

这可以通过添加 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();
   }
}
Run Code Online (Sandbox Code Playgroud)

产生这个程序集:

_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
Run Code Online (Sandbox Code Playgroud)

这是预期的行为,即,可观察的行为foo()相当于:

void foo()
{
   extern int upper_bound;
   upper_bound = 0;
}
Run Code Online (Sandbox Code Playgroud)

  • 仅仅因为块作用域“extern”声明引入的名称对于名称查找不可见,并不意味着它们不引用同一实体。 (4认同)
  • @MikaelPersson除非编译器可以明确证明这些名称*不引用*同一实体(显然不能),否则不允许在假设它们不引用的情况下执行优化。该标准没有区分编译器和链接器;它只关心整个“实现”。 (3认同)
  • 无论您对标准所说的是否正确(我对此还没有意见),在顶部添加“extern”声明无疑是一个好主意,并且更清楚地表达了意图。 (2认同)