c++ - 在实践中,为什么不同的编译器会计算不同的 int x = ++i + ++i; 值?
问题描述
考虑这段代码:
int i = 1;
int x = ++i + ++i;
我们对编译器可能对这段代码做什么有一些猜测,假设它可以编译。
- 两者都
++i
返回2
,导致x=4
. - 一个
++i
返回2
,另一个返回3
,导致x=5
. - 两者都
++i
返回3
,导致x=6
.
对我来说,第二个似乎最有可能。使用 执行两个++
运算符之一i = 1
,i
增加 并2
返回结果。然后用++
执行第二个运算符i = 2
,i
递增,并3
返回结果。然后将2
和3
加在一起给出5
。
但是,我在 Visual Studio 中运行了这段代码,结果是6
. 我试图更好地理解编译器,我想知道什么可能导致6
. 我唯一的猜测是代码可以通过一些“内置”并发执行。调用了两个++
运算符,每个运算符在另一个返回之前递增i
,然后它们都返回3
。这与我对调用堆栈的理解相矛盾,需要解释一下。
编译器可以做哪些(合理的)事情C++
会导致结果4
或结果或6
?
笔记
此示例作为未定义行为的示例出现在 Bjarne Stroustrup 的编程:使用 C++ 的原则和实践 (C++ 14) 中。
见肉桂的评论。
解决方案
编译器获取您的代码,将其拆分为非常简单的指令,然后以它认为最佳的方式重新组合和排列它们。
编码
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++ 给你的结果。
请注意,在两个优化路径中,重要的顺序依赖关系都被保留了,只要指令没有因为什么都不做而被删除。
推荐阅读
- javascript - 将段落拆分为数组 javascript
- sql - 在单个 CTE 上运行多个 SQL 查询
- mysql - 如何在 Moleculer.js 中使用 Sequelize ORM 原始查询(内联或已准备好的 SQL 查询)
- c# - “字段定义中的语法错误。” 尝试创建计算列时
- javascript - JavaScript 模块模式问题 - 方法的 THIS 指向 Window
- java - 集成 Spring Boot 和 Reactor-Kafka 的 KafkaReceiver
- javascript - 如何以首选格式重组包含对象的数组?
- flutter - 如何将小部件的中心 Y 对齐到另一个的顶部边缘
- javascript - 有没有办法为图表中的同一个条形使用 2 种不同的颜色?
- python - 我在 href="#" 中找不到点击事件