分配两个双精度值,保证产生相同的位集模式
is assigning two doubles guaranteed to yield the same bitset patterns?
这里有几篇关于浮点数及其性质的文章。很明显,必须始终谨慎地比较浮点数和双精度数。还讨论了要求平等的问题,建议显然是远离它。
但是,如果有直接分配怎么办:
double a = 5.4;
double b = a;
假设a
是任何非 NaN 值 -a == b
会是假的吗?
答案显然是否定的,但我找不到任何在C++环境中定义这种行为的标准。IEEE-754 指出,具有相等(非 NaN)位集模式的两个浮点数相等。现在这是否意味着我可以继续以这种方式比较我的双打,而不必担心可维护性?我是否必须担心其他编译器/操作系统及其在这些行上的实现?或者也许是一个编译器,可以优化一些位并破坏它们的相等性?
我写了一个小程序,它永远生成和比较非 NaN 随机双精度 - 直到它找到a == b
产生false
的情况。我可以在将来的任何地方和任何时间编译/运行此代码而不必期望停止吗?(忽略字节序并假设符号、指数和尾数位大小/位置保持不变)。
#include <iostream>
#include <random>
struct double_content {
std::uint64_t mantissa : 52;
std::uint64_t exponent : 11;
std::uint64_t sign : 1;
};
static_assert(sizeof(double) == sizeof(double_content), "must be equal");
void set_double(double& n, std::uint64_t sign, std::uint64_t exponent, std::uint64_t mantissa) {
double_content convert;
memcpy(&convert, &n, sizeof(double));
convert.sign = sign;
convert.exponent = exponent;
convert.mantissa = mantissa;
memcpy(&n, &convert, sizeof(double_content));
}
void print_double(double& n) {
double_content convert;
memcpy(&convert, &n, sizeof(double));
std::cout << "sign: " << convert.sign << ", exponent: " << convert.exponent << ", mantissa: " << convert.mantissa << " --- " << n << 'n';
}
int main() {
std::random_device rd;
std::mt19937_64 engine(rd());
std::uniform_int_distribution<std::uint64_t> mantissa_distribution(0ull, (1ull << 52) - 1);
std::uniform_int_distribution<std::uint64_t> exponent_distribution(0ull, (1ull << 11) - 1);
std::uniform_int_distribution<std::uint64_t> sign_distribution(0ull, 1ull);
double a = 0.0;
double b = 0.0;
bool found = false;
while (!found){
auto sign = sign_distribution(engine);
auto exponent = exponent_distribution(engine);
auto mantissa = mantissa_distribution(engine);
//re-assign exponent for NaN cases
if (mantissa) {
while (exponent == (1ull << 11) - 1) {
exponent = exponent_distribution(engine);
}
}
//force -0.0 to be 0.0
if (mantissa == 0u && exponent == 0u) {
sign = 0u;
}
set_double(a, sign, exponent, mantissa);
b = a;
//here could be more (unmodifying) code to delay the next comparison
if (b != a) { //not equal!
print_double(a);
print_double(b);
found = true;
}
}
}
使用 Visual Studio Community 2017 版本 15.9.5
C++标准在 [basic.types]#3 中明确规定:
对于任何平凡可复制的类型
T
,如果指向T
的两个指针指向不同的T
对象obj1
和obj2
,其中obj1
和obj2
都不是潜在重叠的子对象,如果构成obj1
的底层字节([intro.memory])被复制到obj2
中,obj2
随后应保持与obj1
相同的值。
它给出了这个例子:
T* t1p;
T* t2p;
// provided that t2p points to an initialized object ...
std::memcpy(t1p, t2p, sizeof(T));
// at this point, every subobject of trivially copyable type in *t1p contains
// the same value as the corresponding subobject in *t2p
剩下的问题是什么是value
。我们在 [basic.fundamental]#12 中找到(强调我的):
有三种浮点类型:
float
、double
和long double
。double
型提供的精度至少与float
一样多,而long double
型提供的精度至少与double
一样多。float
类型的值集是double
类型的值集的子集;double
类型的值集是long double
类型的值集的子集。浮点类型的值表示形式是实现定义的。
由于C++标准对浮点值的表示方式没有进一步的要求,因此您将从标准中找到所有保证,因为只需要分配来保留值([expr.ass]#2):
在简单赋值(
=
)中,左操作数所指的对象被修改为右操作数的结果,将其值替换为右操作数的结果。
正如您正确观察到的那样,IEEE-754 要求非 NaN、非零浮点数在当且仅当它们具有相同的位模式时进行比较相等。因此,如果您的编译器使用符合 IEEE-754 标准的浮点数,您应该发现非 NaN、非零浮点数的赋值会保留位模式。
事实上,你的代码
double a = 5.4;
double b = a;
永远不应该允许(a == b)
返回 false。但是,一旦你用更复杂的表情替换5.4
,大部分这种美好就会消失。这不是本文的确切主题,但 https://randomascii.wordpress.com/2013/07/16/floating-point-determinism/提到了几种可能的方式,在这些方式中,看起来无辜的代码可以产生不同的结果(这打破了"与位模式相同"的断言)。特别是,您可能会将 80 位中间结果与 64 位舍入结果进行比较,从而可能产生不等式。
这里有一些复杂情况。首先,请注意,标题提出的问题与问题不同。标题问:
分配两个双精度值是否保证产生相同的位集模式?
而问题问:
A == B 会是假的吗?
第一个询问赋值是否会出现不同的位(这可能是由于赋值没有记录与其右操作数相同的值,或者由于赋值使用了表示相同值的不同位模式),而第二个询问赋值是否写入了什么位, 存储的值必须等于操作数。
总的来说,第一个问题的答案是否定的。使用 IEEE-754 二进制浮点格式,非零数值与其位模式编码之间存在一对一映射。但是,这允许赋值可能产生不同位模式的几种情况:
- 正确的操作数是 IEEE-754 −0 实体,但存储了 +0。这不是正确的 IEEE-754 操作,但C++不需要符合 IEEE 754。−0 和 +0 都表示数学零,并且满足C++赋值要求,因此C++实现可以做到这一点。
- IEEE-754 十进制格式在数值及其编码之间具有一对多映射。举例来说,三百可以用直接含义为 3•102的位或直接含义为 300•100 的位表示。同样,由于它们表示相同的数学值,因此在C++标准下,当右操作数是另一个操作数时,允许将一个存储在赋值的左操作数中。
- IEEE-754 包括许多称为 NaN(表示非数字)的非数字实体,C++实现可能会存储与正确操作数不同的 NaN。这可能包括用"规范"NaN 替换任何 NaN 以实现,或者在分配信令 Nan 时,以某种方式指示信号,然后将信令 NaN 转换为安静的 NaN 并存储它。
- 非 IEEE-754 格式可能存在类似的问题。
关于后一个问题,a = b
后a == b
假吗,其中a
和b
都有类型double
,答案是否定的。C++标准确实要求赋值将左操作数的值替换为右操作数的值。所以,在a = b
之后,a
必须具有b
的值,因此它们是相等的。
请注意,C++标准没有对浮点运算的准确性施加任何限制(尽管我只在非规范性注释中看到了这一点)。因此,从理论上讲,人们可能会将浮点值的赋值或比较解释为浮点运算,并说它们不需要是准确性,因此赋值可能会更改值或比较可能会返回不准确的结果。我不认为这是对标准的合理解释;对浮点精度没有限制,旨在允许在表达评估和文库例程中具有自由度,而不是简单的分配或比较。
应该注意的是,上述内容专门适用于从简单double
操作数分配的double
对象。这不应使读者沾沾自喜。几种相似但不同的情况可能会导致数学上看似直观的失败,例如:
float x = 3.4;
之后,表达式x == 3.4
的计算结果一般为假,因为3.4
是一个double
,必须转换为赋值的float
。这种转换会降低精度并更改值。double x = 3.4 + 1.2;
之后,C++标准允许表达式x == 3.4 + 1.2
计算结果为假。这是因为该标准允许以比标称类型要求的更高的精度计算浮点表达式。因此,3.4 + 1.2
可以以long double
的精度进行评估。当结果分配给x
时,标准要求"丢弃"多余的精度,因此该值被转换为double
。与上面的float
示例一样,此转换可能会更改值。然后,比较x == 3.4 + 1.2
可以将x
中的double
值与本质上由3.4 + 1.2
产生的long double
值进行比较。
- 如何在C++中从两个单独的for循环中添加两个数组
- 为什么两个不同的未命名名称空间可以共存于一个cpp文件中
- 当在同一名称空间中有两个具有相同签名的函数时,会发生什么
- 如何返回一个类的两个对象相加的结果
- 如何在C++中将一个无符号的 int 转换为两个无符号的短裤?
- 如何将两个不同矢量的同一位置的两个元素组合在一起
- 两个字符串在 c++ 中不相等
- 在两个类中共享相同的函数调用,并在不需要时避免空实例化
- 两个文件使用彼此的功能-如何解决
- 特征:比较两个稀疏矩阵,其稀疏性模式可能不同
- 分配两个双精度值,保证产生相同的位集模式
- 将两个uint32_t转换为uint64_t,然后根据位模式而不是值变回双精度
- 两个附带的类层次结构-一个好的设计模式
- 两个相同的函数(一个使用模板模式,另一个不使用)
- 在附加模式下使用来自两个不同进程的流
- 在Event Sourcing模式中,您是否有两个不同的类来读取和写入事件?
- 在or条件语句中是否存在强制对两个表达式求值的模式或技巧?
- 为什么我得到两个不同的结果,而使用sregex_iterator与regex变量模式vs构造模式
- 设计模式-如何在C++中组合共享同一基类的两个类
- Qt:如何在大小写不敏感模式下减去QString的两个QSet