我能否保证C++编译器不会重新排列我的计算?

11

我目前正在阅读优秀的双精度和四倍精度算术库文献,在最初几行中我注意到他们用以下方式执行求和:

std::pair<double, double> TwoSum(double a, double b)
{
    double s = a + b;
    double v = s - a;
    double e = (a - (s - v)) + (b - v);
    return std::make_pair(s, e);
}

误差计算依赖于一项事实,即由于IEEE-754浮点数运算的非结合性质,计算完全遵循操作顺序。

如果我在现代优化的C++编译器(例如MSVC或gcc)中编译它,可以确保编译器不会优化掉这种计算方法吗?

其次,在C++标准中是否有任何保证?


2
这不是Javascript。如果规范没有明确定义像这样简单的东西,C++程序员会大声抗议。但由于编译器及其优化器是由人类编写的(目前如此),我会用几种同构的方式进行计算,并确保在将输出提供给神经外科机器人之前它们都是相同的。对于大多数其他用途,只需假定它会表现出可预测的行为即可。 - HostileFork says dont trust SE
@HostileFork:好在这不会用于任何关键应用程序,我正在阅读这个以便自己学习。 - Mike Bailey
我将使用不同的编译器优化设置来测试该代码,并查看结果。 :) 但我打赌无论如何结果都会相同。 - user898058
10个回答

7

我没有看到任何编译器选项可以保证一致的浮点数行为。 - curiousguy
1
以下选项控制编译器关于浮点运算的行为。这些选项在速度和正确性之间进行权衡。所有选项都必须明确启用。 - teambob
1
@teambob:只适用于gcc。Intel C++编译器默认为“-fp-model fast=1”。 - Ben Voigt
@BenVoigt "gcc" 是什么意思? - curiousguy
1
@BenVoigt:是的,这些信息特别针对gcc/g++。感谢您提供有关ICC的有趣信息。 - teambob
显示剩余11条评论

6

是的,在这种情况下是安全的。你只使用了两个"运算符",主要表达式(something)和二元something +/- something(加法)。

C++0x N3092的第1.9节程序执行声明:

只有当运算符确实是可结合或可交换的时,才可以按照通常的数学规则重新分组运算符。

在分组方面,5.1 Primary expressions声明:

括号表达式是一个主表达式,其类型和值与封闭表达式相同。...括号表达式可以在与封闭表达式可以使用的完全相同的上下文中使用,并具有相同的含义,除非另有说明。

我相信引用中的单词"identical"要求符合实现保证它将按指定顺序执行,除非其他顺序可以给出完全相同的结果。

对于加法和减法,5.7 Additive operators部分有:

加法运算符+和-从左到右分组。

因此,标准规定了结果。如果编译器可以确定可以通过不同的操作顺序获得相同的结果,则可以重新排列它们。但无论发生什么,你都无法辨别出差异。


1
"因此,标准规定了结果。" 如果是这样的话,gcc就没有提供任何选项来打开标准行为。 - curiousguy
我很确定那是错误的。如果我没记错,编译器允许生成任何产生正确结果加减1 LSB范围内的代码。 - Ben Voigt
@curiousguy:C++标准中的epsilon与舍入误差相关的数学epsilon是不同的。在您的表达式中,“double_epsilon / 2”根据epsilon在标准中的定义会得到零。请记住,浮点值以指数和尾数存储。如果您改变尾数的值 +/-1,那就是我所说的内容。 - Ben Voigt
@curious,这些引用都没有指定结果的准确性,只有顺序,这也是问题所问的。 - paxdiablo
1
@curious等人,所谓的歧义,是指编译器可以自由地将a+b-c计算为(a+b)-c或者a+(b-c),只要结果在范围内(对于整数来说是精确的,对于浮点数来说不太精确)。但是,它不允许以任何方式计算特定的(a+b)-c,而不是首先计算(a+b),然后再减去c。据我所知,这就是标准中第二个引用中“相同”的含义。每个步骤中的错误都是无关紧要的,因为编译器在这种情况下不允许重新排序,所以无论如何错误都会累积。 - paxdiablo
显示剩余17条评论

5

因为这不是一个答案? - user948581
1
@Tibo:仔细看。我说的是哪个编译器不会保留结果,哪个命令行选项控制它,以及默认设置可能存在问题。我的回答中所有这些都有,如果需要更多细节,您只需单击链接即可。 - Ben Voigt

3

如果使用默认的优化选项,任何编译器都不会错误地假设算术运算符的结合性。

但是要注意FP寄存器的扩展精度

请参考编译器文档,了解如何确保FP值没有扩展精度。


1
如果你真的需要,我认为你可以创建一个 noinline 函数 no_reorder(float x) { return x; },然后使用它代替括号。显然,这不是一个特别高效的解决方案。

0
一般情况下,您应该能够让优化器了解实际操作的属性。
话虽如此,我会充分测试我所使用的编译器。

1
x86编译器也知道每次存储/加载中间FP结果的代价很高。 - curiousguy

0

是的。编译器不会像那样改变块内计算的顺序。


在编译器的规格说明中,如果您编写x = (1+2)*3;任何一个值得信赖的编译器都会产生9的结果。句号。回答OP的问题。如果您有一堆语句,其中之一是a = b * c; d = a * e;编译器将按照它们出现的顺序执行它们。如果a没有初始化并且尝试首先编译第二个语句会发生什么呢? - user898058
3
但是对于 x = 1 + 1e25 - 1e25;,你期望你的“任何值得信赖的编译器”会输出什么? - Ben Voigt
1
@ephaitch:不,事实上,这个问题确切地涉及到算术中十进制精度的行为。 - GManNickG
1
那么,我有点困惑,所以我会避免参与这个对话。因为在重新阅读原帖后,他问的是一旦编译完成,是否可以确保“计算遵循操作顺序”。 - user898058
@epihaitch:十进制精度?这个和十进制有什么关系吗?可能的结果是:0.0(完全相同),1.0(完全相同)或2.0(完全相同)。这取决于操作顺序。1 + 1e25 - 1e25 很可能会产生与 1e25 - 1e25 + 1 不同的结果,后者肯定是1.0(完全相同)。 - Ben Voigt
显示剩余7条评论

0

在编译器优化和处理器乱序执行之间,几乎可以保证事情不会按照您的顺序发生。

然而,也可以保证这永远不会改变结果。C++遵循标准的运算顺序,所有优化都保留了这种行为。

底线:不要担心。编写数学上正确的C++代码并信任编译器。如果出现问题,问题几乎肯定不是编译器造成的。


1
实际上,由于 fp 单元寄存器的扩展精度(特别是在 x86 上),编译器必须将中间结果存储在内存中以获得正确舍入的 fp 结果。但许多编译器并没有这样做! - curiousguy
@curiosguy,你能详细说明一下吗?我一直以为所有FP操作只是在移位操作中使用守卫位来减少舍入误差(正如IEEE标准中所规定的那样),但守卫位中的额外信息在最终舍入后被丢弃。 - riwalk
4
@Stargazer712,有很多程序的输出结果会因为中间结果是存在80位浮点寄存器还是64位双精度浮点数中而产生不同。请参考https://dev59.com/Y2s05IYBdhLWcg3wANLj来查看一个例子。 - Mark Ransom
@curiousguy,Stargazer712:FPU的额外精度确实减少了舍入误差。它不会符合IEEE-754规范所要求的内容。但这并不意味着它是“错误”的,因为C++规范并不要求完全符合IEEE-754的结果。相反,它参考ISO/IEC 10967。 - Ben Voigt
1
如果更改优化级别,这是否会对特定编译器产生任何影响?我不知道有任何一个编译器在x86上默认情况下不会出现错误。 - curiousguy
显示剩余7条评论

-1

根据其他答案,您应该能够依赖编译器做正确的事情 - 大多数编译器允许您编译和检查汇编程序(对于gcc,请使用-S) - 您可能希望这样做以确保您获得所期望的操作顺序。

不同的优化级别(在gcc中,-O _O2等)允许代码重新排列(但是像这样的顺序代码不太可能受到影响) - 但我建议您将该特定代码部分隔离到单独的文件中,以便您可以仅控制计算的优化级别。


1
根据其他答案,你应该能够依赖编译器做正确的事情。但是根据其他评论,你不应该这样做。 - curiousguy

-1

简短的回答是:编译器可能会改变你计算的顺序,但它永远不会改变你程序的行为(除非你的代码使用了具有未定义行为的表达式:http://blog.regehr.org/archives/213

然而,你仍然可以通过停用所有编译器优化(使用gcc的选项“-O0”)来影响这种行为。如果你仍然需要编译器优化你代码的其余部分,你可以将此函数放在单独的“.c”文件中,并使用“-O0”进行编译。 此外,你可以使用一些技巧。例如,如果你将你的代码与外部函数调用交错使用,编译器可能会认为重新排序你的代码是不安全的,因为该函数可能具有未知的副作用。调用“printf”来打印你的中间结果的值将导致类似的行为。

无论如何,除非你有非常好的理由(例如调试),你通常不需要关心这个问题,你应该相信编译器。


“它永远不会改变您程序的行为”这是错误的。函数式编程的行为在大多数编译器上默认情况下并不一致。而且显然GCC甚至没有提供标准符合模式! - curiousguy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接