与C相比,C++中无副作用的无限循环是未定义行为的好处?

wbrvyc0a  于 12个月前  发布在  其他
关注(0)|答案(3)|浏览(120)

在C++循环中,

for(;;) {}

是未定义的行为,而它们在C(?).
P2809R0. Trivial infinite loops are not Undefined Behavior 上,表示这样做有 * 充分的理由 *。有没有一些简单的例子来说明这一点?

erhoui1w

erhoui1w1#

原因只是优化。
如果编译器可以假设所有循环都终止而没有副作用,那么它就不必证明这一点。
如果允许非终止循环,编译器将被允许执行某些优化,只有当它可以证明终止,这是不可能的,所以它会变成模式识别游戏。有什么好处?

**潜在的问题是,非终止性本身是一种副作用。**任何在循环终止后肯定会发生的可观察到的效果,当且仅当循环终止时才被观察到,即使循环没有任何效果。

当然,对于if(expr) return;也可以进行完全相同的推理。编译器不允许将if之后的内容移到if之前,除非它能证明expr为false。但是if是基本的控制流机制,而非终止循环不是(恕我直言)。
取以下代码。

int collatz_conjecture(int i){
    while(i!=1){
        if ((i%2)==0)
            i/=2;
        else
            i=i*3+1;
    }
    return i;
}

int main(){
    collatz_conjecture(10);

    return 5;
}

使用-O3,gcc将其编译为:

collatz_conjecture(int):
        mov     eax, 1
        ret
main:
        mov     eax, 5
        ret

那么,编译器是否证明了Collatz conjecture,以确定它应该对所有数字返回1?当然不是,这只是终止假设允许的优化之一(也是UB可能发生的情况)。循环终止的唯一方式是如果i==1,那么它可以在循环后假设i==1,并将其用于进一步优化->函数始终返回1,因此可以简化为1。
更有用的例子可以是交错复制。如果你有

loop A
loop B

允许编译器即使在不知道A终止的情况下交错它们。许多向量化操作依赖于此假设。
类似地,在循环之前对一些独立的循环后操作进行重新排序假设循环将终止。

pepwfjgg

pepwfjgg2#

主要的好处是规范的简单性。as-if规则不能真正适应这样的概念,即程序可以具有定义的行为,但可能与顺序程序执行明显不一致。此外,C和C++标准的作者使用短语“未定义行为”作为他们认为没有必要行使管辖权的情况的总括,在许多情况下,因为他们希望编译器编写者比委员会更好地理解客户的需求。
大多数有用的优化都是通过指定如果循环中没有单独的操作相对于后面的代码片段进行排序,则整个循环的执行也不需要被视为排序。这对于在无限循环“之后”出现什么代码来说有点“模棱两可”,但它清楚地说明了它允许编译器做什么。除此之外,如果在程序终止之前循环中没有单独的动作被排序,那么整个循环的执行可以完全省略
这样的规则将体现的一个重要原则是,将在循环内的代码和循环外的代码之间引入依赖性的转换也将引入顺序关系,这是本规则所缺少的。如果某个条件为真,则循环将退出,并且循环后的代码检查该条件,则编译器可以使用早期检查的结果来避免重复测试,但这意味着循环后的代码依赖于循环内计算的值。
下面是一个具体的例子,说明了适用这一规则的有用和鲁莽的方式:

char arr[65537];
unsigned test(unsigned x, unsigned y)
{
    unsigned i=1;
    while((i & y) != x)
        i*=17;
    return i;
}
void test2(unsigned x)
{
    test(x, 65535);
    if (x < 65536)
        arr[x] = 2;
}

在将test内联到test2中时,编译器可以应用两个 * 单独 * 有用的优化:
1.编译器可以识别出,在x小于65536的情况下,x == (i & 65535)是否只能报告“true”,从而使test2()中的if测试变得多余。
1.编译器可以认识到,由于循环的唯一效果是计算i,并且当从test2()中调用test()时,i的值最终将被忽略,因此循环的代码是冗余的。
消除循环,同时保持隔离的if可能比相反的做法更好,但是代码维护可能是基本要求的能力-它不会写入arr超过元素65535-依赖于循环或if测试被保留。两者中的任何一个在另一个存在的情况下都是多余的,但如果没有任何一个,另一个就必不可少了。
请注意,给予编译器重新排序代码的自由,同时保持数据依赖性,这并不排除正确的应用程序在输入一些可能的输入时陷入无限循环的可能性,如果在某些情况下需要使用外部手段终止应用程序是可以接受的。然而,允许它们重新排序代码而不考虑产生的数据依赖性,将具有讽刺的效果,即加速不能依赖于满足应用程序要求的主要“错误”程序,并且对大多数添加额外检查或虚拟副作用(否则不需要满足应用程序要求)以防止无限循环的程序没有任何好处。
PS--作为两段代码在另一段代码存在时单独冗余的一般概念的另一个例子,考虑以下函数:

double x,y,z;
void f1(void) {
  x=sin(z);
}
void f2(void)
{
  f1();
  y=sin(z);
  x=0;
}

赋值x=sin(z);在编写的代码中是冗余的,并且可以被优化掉,因为没有任何东西使用存储在x中的值。在f2中计算sin(z)在代码中是多余的,可以用y=x;代替,因为x已经保存了需要存储到y中的值。然而,显而易见的是,任何一种转变本身都可以合法地适用,这并不意味着两者都可以合法地一起适用。这个原则应该适用于第一个示例代码片段中使用的优化类型的想法可能对编写标准的人来说是显而易见的,但不幸的是,对编写clang和gcc的人来说不是。

mzsu5hc0

mzsu5hc03#

在C和C++中,没有副作用的无限循环会表现出未定义的行为。然而,这两种语言在编译器处理这种情况的方式上可能略有不同。最后,不建议依赖任何一种语言中的未定义行为,因为它可能导致不可预测的结果。最好避免编写有意依赖未定义行为的代码。

相关问题