硬编码 double 的最低有效字节是一种好的舍入策略吗?

Is hardcoding least significant byte of a double a good rounding strategy?

我有一个函数可以进行一些数学计算并 returning double。由于 std::exp 实现不同 (),它最终在 Windows 和 Android 下得到不同的结果。 e-17 舍入差异得到传播,最终它不仅仅是我得到的舍入差异(结果最终可以将 2.36 更改为 2.47)。当我将结果与一些预期值进行比较时,我希望此函数在所有平台上 return 获得相同的结果。

所以我需要四舍五入我的结果。最简单的解决方案显然是(据我在网上找到的)std::ceil(d*std::pow<double>(10,precision))/std::pow<double>(10,precision)。但是,我觉得根据平台的不同,这仍然会导致不同的结果(而且,很难决定 precision 应该是什么)。

我想知道对 double 的最低有效字节进行硬编码是否是一个很好的舍入策略。

这个快速测试似乎表明 "yes":

#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)

我的问题是:

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


编辑,我可能会重新表述我的问题,因为人们在问为什么我有不同的价值观以及为什么我希望它们相同。

假设您从计算中得到一个 double,由于特定于平台的实现(例如 std::exp),最终可能会得到不同的值。如果您想修复那些不同的 double 以最终在所有平台上具有完全相同的内存表示 (1),并且您希望尽可能降低精度,那么修复最低有效字节是一个好方法? (因为我觉得四舍五入到任意给定的精度可能会丢失比这个技巧更多的信息)。

(1) "same representation",我的意思是如果您将它转换为 std::bitset,您希望看到所有平台的相同位序列。

不,四舍五入不是消除小错误或保证与错误执行的计算一致的策略。

对于将数字线分成范围的任何切片,您将成功消除大部分微小的偏差(通过将它们放在同一个桶中并固定到相同的值),但是如果您的原始值对,您会大大增加偏差跨越边界。

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

0x1.mmmmmmm100

0x1.mmmmmmm0ff

只有一个 ULP 的偏差...但在四舍五入后,它们相差 256 ULP。糟糕!

您的函数将因别名而无法运行。

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 强制转换为 unsigned char*,因为 char* 强制转换是别名规则的例外。这对于 read、write、memcpy 等函数来说是必需的,以便它们可以将值复制到字节表示形式或从字节表示形式复制值。

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

Is unsigned char* temp = (unsigned char*) &rounded safe or is there an undefined behaviour here, and why?

定义明确,通过 unsigned char 的别名是 allowed

is such a round function safe and accurate for all input?

没有。您无法使用 truncating/rounding 完美解决此问题。考虑一下,一个实现给出 0x.....0ff,另一个实现给出 0x.....100。将 lsb 设置为 0x00 将使原来的 1 ulp 差异为 256 ulp。

没有舍入算法可以解决这个问题。

您有两个选择:

  • 不要使用浮点数,使用其他方式(例如定点数)
  • 将浮点库嵌入到您的应用程序中,它仅使用基本的浮点运算(+、-、*、/、sqrt),并且不使用 -ffast-math 或任何等效选项。这样,如果您在 IEEE-754 兼容平台上,浮点结果应该是相同的,因为 IEEE-754 要求计算基本操作 "perfectly"。这意味着好像该操作以无限精度计算,然后四舍五入到结果表示。

顺便说一句,如果输入 1e-17 差异意味着巨大的输出差异,那么您的 problem/algorithm 就是 ill-conditioned,这通常应该避免,因为它通常不会给您有意义的结果。

你的所作所为完全、完全被误导了。

您的问题不在于您得到不同的结果(2.36 与 2.47)。您的问题是,这些结果中至少有一个(很可能同时有两个)存在大量错误。您的 Windows 和 Android 结果不仅不同,而且是错误的。 (至少其中之一,你不知道是哪一个)。

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

您正在尝试的只是使舍入误差大 256 倍,如果两个不同的结果以 ..1ff 和 ..200 十六进制结尾,那么您将它们更改为 ..180 和 . ...280,因此即使是略有不同的数字之间的差异也可以增长 256 倍。

并且在 bigendian 机器上,您的代码将直接运行!!!