为什么我为std :: exp获得特定于平台的结果?

jpo*_*o38 6 c++ rounding-error exp

我有一个程序在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;
}
Run Code Online (Sandbox Code Playgroud)

在Windows下,使用Visual 2015编译,我得到输出:

var is -7.87234042553191493141184764681
std::exp(var) is 0.00038114128472300899284561093161
Run Code Online (Sandbox Code Playgroud)

在使用g ++ NDK r11b编译的Android/armv7下,我得到输出:

var is -7.87234042553191493141184764681
std::exp(var) is 0.000381141284723008938635502307335
Run Code Online (Sandbox Code Playgroud)

所以从e-20开始的结果是不同的:

PC:      0.00038114128472300899284561093161
Android: 0.000381141284723008938635502307335
Run Code Online (Sandbox Code Playgroud)

请注意,我的程序执行了大量的数学运算,我只注意到std::exp对同一输入产生不同的结果......并且仅针对某些特定的输入值(没有调查这些值是否具有相似的属性),对于大多数,结果是一样的.

  • 这种行为是"预期的",在某些情况下是否无法保证获得相同的结果?
  • 是否有一些编译器标志可以解决这个问题?
  • 或者我是否需要将结果四舍五入以在两个平台上都相同?那么四舍五入的好策略是什么?因为如果var在非常小的情况下输入,在e-20处进行舍入将会丢失太多信息?

编辑:我认为我的问题不是重复的浮点数学被打破了吗?.我在两个平台上得到完全相同的结果,只是std::exp对于某些特定值产生不同的结果.

LoP*_*TaL 6

该标准没有定义应该如何实现exp函数(或任何其他数学库函数1),因此每个库实现可以使用不同的计算方法.

例如,Android C库(仿生)在区间[0,0.34658]上使用特殊有理函数逼近exp(r)并缩减结果.

可能Microsoft库正在使用不同的计算方法(无法找到有关它的信息),从而导致不同的结果.

此外,库可以采用动态加载策略(即加载.dll包含实际实现的策略),以便利用不同的硬件特定功能,使得结果更加难以预测,即使使用相同的编译器也是如此.

为了在两个(所有)平台上获得相同的实现,您可以使用自己的exp函数实现,因此不依赖于不同库的不同实现.

考虑到处理器可能采用不同的舍入方法,这也会产生不同的结果.

1 这些有一些例外,用于isntance sqrt函数或std :: fma以及一些舍入函数和基本算术运算

  • 有一些数学库函数_are_指定,以保证精确(如,最接近可表示)的结果.这尤其包括[`sqrt`](https://en.cppreference.com/w/cpp/numeric/math/sqrt),还有[`std :: fma`](https://en.cppreference.com)/w/cpp/numeric/math/fma)和各种舍入函数,当然还有基本的算术运算符.舍入模式也不一定是每个"不同的体系结构" - 您可以在今天的通用硬件上基于每个线程设置舍入模式. (3认同)