为什么我使用std::exp时会得到特定于平台的结果?

7

我有一个程序,在Android和Windows下会产生稍微不同的结果。由于我要将输出数据与包含预期结果的二进制文件进行验证,即使是非常小的差异(四舍五入问题),也是很烦人的,我必须找到一种解决方法。

下面是一个样例程序:

#include <iostream>
#include <iomanip>
#include <bitset>

int main( int argc, char* argv[] )
{
    // this value was identified as producing different result when used as parameter to std::exp function
    unsigned char val[] = {158, 141, 250, 206, 70, 125, 31, 192};

    double var = *((double*)val);

    std::cout << std::setprecision(30);

    std::cout << "var is " << var << std::endl;
    double exp_var = std::exp(var);
    std::cout << "std::exp(var) is " << exp_var << std::endl;
}

在Windows下,使用Visual 2015编译,我得到了以下输出:
var is -7.87234042553191493141184764681
std::exp(var) is 0.00038114128472300899284561093161

在 Android/armv7 下,使用 g++ NDK r11b 编译时,我得到了以下输出:
var is -7.87234042553191493141184764681
std::exp(var) is 0.000381141284723008938635502307335

因此,从e-20开始,结果会有所不同。
PC:      0.00038114128472300899284561093161
Android: 0.000381141284723008938635502307335

请注意,我的程序进行了很多数学运算,我只注意到std::exp在相同的输入下产生不同的结果...而且只有在一些特定的输入值(没有调查这些值是否具有类似的属性)中才会出现这种情况,对于大多数输入值,结果是相同的。
  • 这种行为是否是"预期"的,在某些情况下不能保证具有相同的结果吗?
  • 是否有一些编译器标志可以修复这个问题?
  • 还是我需要将结果四舍五入以使其在两个平台上相同?那么对于四舍五入,应该采取什么样的策略?因为如果输入var非常小,则随意四舍五入到e-20会失去太多信息?

编辑:我认为我的问题与浮点数运算是否出错不重复。我在两个平台上得到完全相同的结果,只有在某些特定值的std::exp产生不同的结果。


4
你的代码中有 double var = *((double*)val) 这行代码使用了 UB。 - Jarod42
第17个有效数字后出现了差异。您是否已经通过numeric_limits检查实现是否保证double的相同精度?由于您使用输入值的二进制编码(强制转换),您确定它在两个平台上都符合预期的规范化吗(即,使用硬编码的double文字时差异是否相同)? - Christophe
@Christophe:正如评论所述,强制转换的输入值仅用于测试。如果使用 double var = -7.87234042553191493141184764681;,我遇到了同样的问题。 - jpo38
3
@jpo38 好的,差异在第17个有效数字处出现。因此它是按设计工作的:您期望比提供的更高的精度。 - Christophe
1
@Bob__:是的,我最初尝试了那个。但是来自ndk r11b的g++不接受std::hexfloat.... - jpo38
显示剩余15条评论
1个回答

6
该标准未定义 exp 函数(或任何其他数学库函数1)应如何实现,因此每个库实现可能使用不同的计算方法。
例如,Android C库(bionic)在区间[0,0.34658]上使用特殊有理函数对exp(r)进行近似,并将结果缩小。
可能微软库正在使用不同的计算方法(找不到相关信息),因此导致不同的结果。
此外,库可以采用动态加载策略(即加载包含实际实现的.dll),以利用不同的硬件特定功能,即使使用同一编译器,也会使结果变得更加难以预测。
为了在两个(或多个)平台上获得相同的实现,您可以使用自己的 exp 函数实现,因此不依赖于不同库的实现。
请注意,处理器可能采用不同的舍入方法,这也会导致不同的结果。 1 有一些例外情况,例如 sqrt 函数或 std:: fma 和一些舍入函数和基本算术操作


3
有一些数学库函数是被规定保证得到精确的结果(即最接近可表示的结果)。其中显著包括 sqrt,还有例如 std::fma 和各种舍入函数,当然也包括基本算术运算符。舍入模式也不一定是针对“不同架构”的 - 在普通硬件上,您可以按线程设置舍入模式。 - Max Langhof
1
值得一提的是,微软的实现可能会在运行时调用一些核心的 .dll 来计算 exp。也就是说,计算方法只有在你在特定的机器上运行程序并使用该(机器特定的).dll 后才能知道,而在编译代码时是无法知道的。这样他们就可以利用每台机器上不同的 CPU 特性(例如矢量化),但结果变得更加不可预测。 - Max Langhof
@MaxLanghof 给答案添加了所有的注释。 - LoPiTaL
据我所知,这更像是硬件问题。在PC的情况下,当FPU计算exp时,计算是在比double表示更大的类型上执行的,当结果被获取时,它会被舍入为double。现在,在ARM处理器上,FPU非常有限,因此实际计算是在不太精确的浮点类型上执行的(甚至可能比double还要小)。 - Marek R
显示剩余2条评论

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