代码之家  ›  专栏  ›  技术社区  ›  jpo38

硬编码双精度数的最低有效字节是一个好的舍入策略吗?

  •  0
  • jpo38  · 技术社区  · 7 年前

    我有一个函数做一些数学计算,然后返回 double . 在windows和android下,由于 std::exp 实现开始不同( Why do I get platform-specific result for std::exp? )。E-17舍入差被传播,最终它不仅仅是我得到的舍入差(结果最终可能会改变2.36到2.47)。当我将结果与一些期望值进行比较时,我希望此函数在所有平台上返回相同的结果。

    所以我需要对我的结果进行四舍五入。要做到这一点,最简单的解决办法显然是(就我在网上所能找到的)做到这一点。 std::ceil(d*std::pow<double>(10,precision))/std::pow<double>(10,precision) . 但是,我觉得根据平台的不同,最终还是会有不同的结果(而且,很难决定 precision 应该是)。

    我想知道是否硬编码 双重的 可能是一个很好的舍入策略。

    这个快速测试似乎表明“是的”:

    #include <iostream>
    #include <iomanip>
    
    double roundByCast( double d )
    {
        double rounded = d;
        unsigned char* temp = (unsigned char*) &rounded;
        // changing least significant byte to be always the same
        temp[0] = 128;
        return rounded;
    }
    
    void showRoundInfo( double d, double rounded )
    {
        double diff = std::abs(d-rounded);
        std::cout << "cast: " << d << " rounded to " << rounded << " (diff=" << diff << ")" << std::endl;
    }
    
    void roundIt( double d )
    {
        showRoundInfo( d, roundByCast(d) );
    }
    
    int main( int argc, char* argv[] )
    {
        roundIt( 7.87234042553191493141184764681 );
        roundIt( 0.000000000000000000000184764681 );
        roundIt( 78723404.2553191493141184764681 );
    }
    

    此输出:

    cast: 7.87234 rounded to 7.87234 (diff=2.66454e-14)
    cast: 1.84765e-22 rounded to 1.84765e-22 (diff=9.87415e-37)
    cast: 7.87234e+07 rounded to 7.87234e+07 (diff=4.47035e-07)
    

    我的问题是:

    • unsigned char* temp = (unsigned char*) &rounded 这里安全还是有未定义的行为?为什么?
    • 如果没有ub(或者如果没有ub有更好的方法可以做到这一点),那么对于所有输入来说,这样一个圆函数是安全和准确的吗?

    注意:我知道浮点数不准确。请不要标记为的副本 Is floating point math broken? Why Are Floating Point Numbers Inaccurate? .我理解为什么结果不同,我只是在寻找一种方法,使它们在所有目标平台上都相同。


    编辑,我可以重新表述我的问题,因为人们问我为什么我有不同的价值观,为什么我希望他们是一样的。

    假设你得到一个 双重的 由于平台特定的实现(如 性病:: )如果你想修复那些不同的 双重的 为了在所有平台上拥有完全相同的内存表示(1),并且您希望尽可能降低最低的精度,那么,固定最低有效字节是一种好方法吗?(因为我觉得四舍五入到任意给定的精度可能会比这个技巧释放更多的信息)。

    (1)通过“相同的表示”,我的意思是如果你把它转换成 std::bitset ,您希望看到所有平台的相同位序列。

    4 回复  |  直到 7 年前
        1
  •  3
  •   geza    7 年前

    unsigned char*temp=(unsigned char*)和rounded是安全的还是这里有未定义的行为,为什么?

    它被很好地定义为 unsigned char allowed .

    这样一个圆函数对所有输入都安全和准确吗?

    不,您不能完美地解决截断/舍入这一问题。考虑一下,一个实现 0x.....0ff 和另一个 0x.....100 . 将LSB设置为 0x00 将使原来的1 ulp差异为256 ulp。

    没有取整算法可以解决这个问题。

    您有两种选择:

    • 不要使用浮点,使用其他方法(例如,定点)
    • 在应用程序中嵌入一个浮点库,它只使用基本的浮点运算(+、-、*、/、sqrt),而不使用 -ffast-math 或任何同等选项。这样,如果您在与IEEE-754兼容的平台上,浮点结果应该是相同的,因为IEEE-754要求基本操作应该“完美地”计算。它的意思是,好像操作是以无限的精度计算的,然后四舍五入为结果表示。

    btw,如果输入 1e-17 差异意味着巨大的输出差异,那么您的问题/算法是 ill-conditioned 这通常应该避免,因为它通常不会给您带来有意义的结果。

        2
  •  5
  •   Ben Voigt    7 年前

    不,舍入不是一种消除小误差的策略,也不是保证与误差计算一致。

    对于将数字线切片到范围内的任何情况,您将成功地消除最轻微的偏差(将它们放在同一个桶中并夹紧到相同的值),但如果原始值对跨过边界,则会大大增加偏差。

    在您的特定情况下,硬编码最低有效字节,即非常接近的值

    0x1.mmmmmmm100
    

    0x1.mmmmmmm0ff
    

    只有一个极限偏差…但在四舍五入后,它们相差256 ulp。哎呀!

        3
  •  2
  •   gnasher729    7 年前

    你所做的完全,完全被误导了。

    你的问题不是你得到了不同的结果(2.36比2.47)。你的问题是,这些结果中至少有一个,可能两者都有巨大的错误。你的windows和android结果不仅不同,而且是错误的。(至少有一个,你不知道是哪一个)。

    找出为什么你会得到这些巨大的误差,并改变你的算法,以避免大量增加微小的舍入误差。或者你有一个内在混乱的问题,在这种情况下,结果之间的差异实际上是非常有用的信息。

    你所尝试的只是使舍入误差大256倍,如果两个不同的结果以……1ff和……200十六进制结尾,那么你将它们更改为……180和……280,这样即使是稍微不同的数字之间的差也可以增加一个因子256。

    在一台bigendian机器上,你的代码会变成kaboom!!!!

        4
  •  0
  •   Zan Lynx    7 年前

    您的函数由于别名而无法工作。

    double roundByCast( double d )
    {
        double rounded = d;
        unsigned char* temp = (unsigned char*) &rounded;
        // changing least significant byte to be always the same
        temp[0] = 128;
        return rounded;
    }
    

    允许为temp强制转换为无符号char*,因为char*强制转换是别名规则的例外。这对于诸如read、write、memcpy等函数是必需的,这样它们就可以在字节表示之间复制值。

    但是,不允许您写入temp[0],然后假定舍入已更改。您必须创建一个新的双变量(在堆栈上很好),并将memcpy temp返回到该变量。