首页 > 解决方案 > 在实践中,为什么不同的编译器会计算不同的 int x = ++i + ++i; 值?

问题描述

考虑这段代码:

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

我们对编译器可能对这段代码做什么有一些猜测,假设它可以编译。

  1. 两者都++i返回2,导致x=4.
  2. 一个++i返回2,另一个返回3,导致x=5.
  3. 两者都++i返回3,导致x=6.

对我来说,第二个似乎最有可能。使用 执行两个++运算符之一i = 1i增加 并2返回结果。然后用++执行第二个运算符i = 2i递增,并3返回结果。然后将23加在一起给出5

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

编译器可以做哪些(合理的)事情C++会导致结果4或结果或6

笔记

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

肉桂的评论

标签: c++undefined-behavior

解决方案


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

编码

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++ 给你的结果。

请注意,在两个优化路径中,重要的顺序依赖关系都被保留了,只要指令没有因为什么都不做而被删除。


推荐阅读