我有一个函数,可以进行一些数学计算并返回double
。由于std::exp
实现的差异(Why do I get platform-specific result for std::exp?),在Windows和Android下最终结果不同。 e-17的舍入差异得到传播,最后我得到的不仅仅是舍入差异(结果最终可以将2.36更改为2.47)。当我将结果与某些期望值进行比较时,我希望此函数在所有平台上返回相同的结果。
所以我需要四舍五入。显然,最简单的解决方案是std::ceil(d*std::pow<double>(10,precision))/std::pow<double>(10,precision)
。但是,我觉得根据平台的不同,结果仍然可能会不同(而且,很难确定precision
应该是什么。)
我想知道对double
的最低有效字节进行硬编码是否是一个好的舍入策略。
此快速测试似乎表明“是”:
#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
是安全的还是在这里存在未定义的行为,为什么?注意:我知道浮点数不正确。请不要将Is floating point math broken?或Why Are Floating Point Numbers Inaccurate?标记为重复项。我了解为什么结果会有所不同,我只是在寻找一种使所有目标平台上的结果都相同的方法。
编辑,我可能会重新提出我的问题,因为人们都在问为什么我拥有不同的价值观,为什么我希望它们保持相同。
假设您从计算中获得double
,由于特定于平台的实现(例如std::exp
),最终可能会得到不同的值。如果要修复那些不同的double
以在所有平台上最终具有完全相同的内存表示(1),并且想要放开尽可能低的精度,那么,修复最低有效字节是一个好方法? (因为我认为舍入到任意给定的精度可能会比该技巧丢失更多信息)。
(1)“表示相同”是指,如果将其转换为std::bitset
,则希望在所有平台上看到相同的位序列。
答案 0 :(得分:5)
不,舍入不是消除小错误或保证与执行错误的计算相一致的策略。
对于将数字线划分为多个范围,您将成功消除大多数细微的偏差(通过将它们放在相同的存储桶中并夹紧为相同的值),但是如果原始值对跨越a,则会大大增加偏差边界。
在您对最低有效字节进行硬编码的特定情况下,非常接近的值
0x1.mmmmmmm100
和
0x1.mmmmmmm0ff
仅有一个ULP的偏差...但是在四舍五入后,它们相差256 ULP。糟糕!
答案 1 :(得分:3)
unsigned char * temp =(unsigned char *)是否四舍五入,还是这里有未定义的行为,为什么?
定义明确,因为通过unsigned char
的别名为allowed。
这样的舍入函数对于所有输入是否安全且准确?
不。您无法通过截断/舍入完美解决此问题。考虑一下,一个实现给出0x.....0ff
,而另一个实现0x.....100
。将lsb设置为0x00
将使原来的1 ulp差异变为256 ulps。
没有舍入算法可以解决这个问题。
您有两个选择:
-ffast-math
或任何等效选项。这样,如果您使用的是IEEE-754兼容平台,则浮点结果应该是相同的,因为IEEE-754要求基本操作应“完美”地进行计算。这意味着该运算似乎是在无限精度下计算的,然后四舍五入为结果表示形式。顺便说一句,如果输入1e-17
的差异意味着巨大的输出差异,那么您的问题/算法是ill-conditioned,通常应避免使用它,因为它通常不会给您带来有意义的结果。 / p>
答案 2 :(得分:2)
您正在做的事情完全是完全被误导的。
您的问题不是您得到不同的结果(2.36对2.47)。您的问题是这些结果中的至少一个(可能同时有两个)都存在大量错误。您的Windows和Android结果不仅不同,而且是错误的。 (其中至少有一个,而且您不知道是哪一个。)
找出为什么会遇到这些大量错误并更改算法以不大量增加微小的舍入错误。或者您遇到了一个固有的混乱问题,在这种情况下,结果之间的差异实际上是非常有用的信息。
您要尝试的操作使舍入误差大256倍,并且如果两个不同的结果以.... 1ff和.... 200十六进制结尾,则将其更改为.... 180和... .280,因此即使数字略有不同也可以相差256。
在bigendian机器上,您的代码将直接变为kaboom !!!
答案 3 :(得分:0)
由于别名,您的功能无法使用。
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 *强制转换是别名规则的例外。对于读取,写入,memcpy等功能而言,这是必需的,以便它们可以在字节表示之间来回复制值。
但是,不允许您写入temp [0],然后假定舍入已更改。您必须创建一个新的double变量(在堆栈上就可以了)并将memcpy temp返回给它。