wim*_*aan 43 c c++ optimization loops undefined-behavior
在 C++ 循环中为
for(;;) {}
Run Code Online (Sandbox Code Playgroud)
是未定义的行为,但它们不在 C(?) 中。
在 P2809R0上。平凡的无限循环不是未定义的行为,它表示这样做有充分的理由。有没有一些简单的例子可以清楚地说明这一点?
Qui*_*mby 60
原因只是优化。
如果编译器可以假设所有没有副作用的循环都终止,则不必证明这一点。
如果允许非终止循环,则只有在能够证明终止的情况下才允许编译器执行某些优化,这通常是不可能的,因此它将变成模式识别游戏。又有什么好处呢?
根本问题是,不终止是其自身的一种副作用。当且仅当循环终止时,才会观察到循环终止后肯定会发生的任何可观察到的效果,即使循环没有任何效果。
当然,可以对 进行完全相同的推理if(expr) return;。编译器不允许移动之前之后的内容if,if除非它可以证明expr是错误的。但这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;
}
Run Code Online (Sandbox Code Playgroud)
使用-O3,gcc 将其编译为:
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;
}
Run Code Online (Sandbox Code Playgroud)
那么,编译器是否证明了Collatz 猜想以确定它应该返回1所有数字?当然不是,这只是终止假设允许的优化之一(也是UB可能发生的地方)。循环终止的唯一方法是如果i==1它可以i==1在循环之后假设并使用它进行进一步优化 -> 函数总是返回 1,因此可以减少到它。
更有用的例子可以是交错复制。如果你有
collatz_conjecture(int):
mov eax, 1
ret
main:
mov eax, 5
ret
Run Code Online (Sandbox Code Playgroud)
即使编译器不知道终止,也可以将它们交错A。许多矢量化操作都依赖于这个假设。
类似地,在循环之前对一些独立的循环后操作进行重新排序会假定循环将终止。
主要好处是规范简单。假设规则不能真正适应这样的概念:程序可以具有已定义的行为,但可能与顺序程序执行明显不一致。此外,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;
}
Run Code Online (Sandbox Code Playgroud)
编译器在内联到 时可以应用两种单独有用的优化:testtest2
编译器可以认识到,查看是否x == (i & 65535)只能在x小于 65536 的情况下报告“true”的测试,从而使该if测试变得test2()多余。
编译器可以认识到,由于循环的唯一作用是计算,并且当从 内部调用时, willi的值i最终会被忽略,因此循环的代码是多余的。test()test2()
在保持隔离的同时消除循环if可能比相反的做法更好,但是代码能否满足基本要求(即不写入arr过去的元素 65535)取决于循环或if保留的测试。任何一个在另一个存在的情况下都是多余的,但任何一个的缺席都会使另一个变得重要。
请注意,如果在某些情况下需要使用外部手段终止应用程序,那么在保留数据依赖性的同时,给予编译器重新排序代码的自由并不能排除正确的应用程序在输入一些可能的输入时可能陷入无限循环的可能性。可以接受。然而,允许它们重新排序代码而不考虑结果数据依赖性,将会产生具有讽刺意义的效果,即加速主要是“错误”的程序,这些程序不能依赖于满足应用程序的要求,并且对大多数添加额外检查或操作的程序没有任何好处。虚拟副作用(否则不需要满足应用程序要求)以防止无限循环。
PS——作为两段代码在另一段代码存在的情况下各自冗余的一般概念的进一步示例,请考虑以下函数:
double x,y,z;
void f1(void) {
x=sin(z);
}
void f2(void)
{
f1();
y=sin(z);
x=0;
}
Run Code Online (Sandbox Code Playgroud)
该赋值x=sin(z);在编写的代码中是多余的,并且可以进行优化,因为没有任何内容使用存储在x. sin(z)在编写的代码中, within的计算f2是多余的,可以替换为,y=x;因为x它已经保存了需要存储到 中的值y。然而,显而易见的是,任一变换本身可以合法地应用这一事实并不意味着两者可以合法地一起应用。这个原则应该适用于第一个示例片段中使用的优化类型,这一想法对于编写标准的人来说可能是显而易见的,但不幸的是对于编写 clang 和 gcc 的人来说却不是。
| 归档时间: |
|
| 查看次数: |
3611 次 |
| 最近记录: |