实际上,为什么不同的编译器会计算出不同的 int x = ++i + ++i; 值?

2024-02-10

考虑这段代码:

int i = 1;
int x = ++i + ++i;

假设它可以编译,我们对编译器可能会对该代码执行的操作有一些猜测。

  1. both ++i return 2, 导致x=4.
  2. one ++i回报2和其他回报3, 导致x=5.
  3. both ++i return 3, 导致x=6.

对我来说,第二种可能性最大。两者之一++运算符的执行方式为i = 1, the i递增,结果2被返回。然后是第二个++运算符的执行方式为i = 2, the i递增,结果3被返回。然后2 and 3加在一起得到5.

但是,我在 Visual Studio 中运行了这段代码,结果是6。我试图更好地理解编译器,我想知道什么可能会导致以下结果6。我唯一的猜测是代码可以通过一些“内置”并发来执行。他们俩++调用运算符,每个运算符都会递增i在对方回来之前,然后他们都回来了3。这与我对调用堆栈的理解相矛盾,需要进行解释。

什么(合理的)事情可以C++编译器这样做会导致结果4或结果或6?

Note

此示例作为 Bjarne Stroustrup 的《编程:使用 C++ 的原理与实践》(C++ 14) 中的未定义行为示例出现。

See 肉桂的评论 https://stackoverflow.com/questions/62185373/in-practice-why-would-different-compilers-compute-different-values-of-int-x/62195002#comment109998211_62195002.


编译器获取您的代码,将其拆分为非常简单的指令,然后以它认为最佳的方式重新组合和排列它们。

The code

int i = 1;
int x = ++i + ++i;

由以下指令组成:

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
5. read i as tmp2
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
9. read i as tmp4
10. add tmp2 and tmp4, as tmp5
11. store tmp5 in x

但是,尽管这是我编写的编号列表,但只有少数排序依赖关系这里: 1->2->3->4->5->10->11 和 1->6->7->8->9->10->11 必须保持其相对顺序。除此之外,编译器可以自由地重新排序,也许还可以消除冗余。

例如,您可以按如下方式对列表进行排序:

1. store 1 in i
2. read i as tmp1
6. read i as tmp3
3. add 1 to tmp1
7. add 1 to tmp3
4. store tmp1 in i
8. store tmp3 in i
5. read i as tmp2
9. read i as tmp4
10. add tmp2 and tmp4, as tmp5
11. store tmp5 in x

为什么编译器可以这样做呢?因为增量的副作用没有排序。但现在编译器可以简化:例如,4 中有一个死存储:该值立即被覆盖。另外,tmp2 和 tmp4 实际上是同一件事。

1. store 1 in i
2. read i as tmp1
6. read i as tmp3
3. add 1 to tmp1
7. add 1 to tmp3
8. store tmp3 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

现在与 tmp1 相关的所有内容都是死代码:它从未被使用过。并且 i 的重读也可以被消除:

1. store 1 in i
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
10. add tmp3 and tmp3, as tmp5
11. store tmp5 in x

看,这段代码短得多。优化器很高兴。程序员不是,因为 i 只增加了一次。哎呀。

让我们看看编译器可以做的其他事情:让我们回到原始版本。

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
5. read i as tmp2
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
9. read i as tmp4
10. add tmp2 and tmp4, as tmp5
11. store tmp5 in x

编译器可以像这样重新排序:

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
5. read i as tmp2
9. read i as tmp4
10. add tmp2 and tmp4, as tmp5
11. store tmp5 in x

然后再次注意到 i 被读取了两次,因此消除其中之一:

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

这很好,但它可以更进一步:它可以重用 tmp1:

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
6. read i as tmp1
7. add 1 to tmp1
8. store tmp1 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

那么就可以消除6中i的重读:

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
7. add 1 to tmp1
8. store tmp1 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

现在 4 是一个死店:

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
7. add 1 to tmp1
8. store tmp1 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

现在 3 和 7 可以合并为一条指令:

1. store 1 in i
2. read i as tmp1
3+7. add 2 to tmp1
8. store tmp1 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

删除最后一个临时的:

1. store 1 in i
2. read i as tmp1
3+7. add 2 to tmp1
8. store tmp1 in i
10. add tmp1 and tmp1, as tmp5
11. store tmp5 in x

现在您就得到了 Visual C++ 给出的结果。

请注意,在两个优化路径中,只要指令没有因不执行任何操作而被删除,就保留了重要的顺序依赖性。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

实际上,为什么不同的编译器会计算出不同的 int x = ++i + ++i; 值? 的相关文章