比较 uint64_t 和 float 的数值等价性
Posted
技术标签:
【中文标题】比较 uint64_t 和 float 的数值等价性【英文标题】:Comparing uint64_t and float for numeric equivalence 【发布时间】:2015-09-27 17:11:35 【问题描述】:我正在编写一个协议,它使用RFC 7049 作为其二进制表示。标准规定,如果它们的数值等于相应的 64 位数字,则协议可以使用数字的 32 位浮点表示。转换不得导致精度损失。
哪些 32 位浮点数可以大于 64 位整数并在数值上与它们等价? 比较float x; uint64_t y; (float)x == (float)y
是否足以确保值相等?这种比较会成立吗?
RFC 7049 §3.6. Numbers
就本规范而言,所有数字表示 对于相同的数值是等价的。这意味着一个 编码器可以将浮点值 0.0 编码为整数 0。 但是,这也意味着期望找到的应用程序 如果编码器,整数值只能找到浮点值 决定这些是可取的,例如当浮点值是 比 64 位整数更紧凑。
【问题讨论】:
What 32-bit float numbers can be bigger than 64-bit integer and numerically equivalent with them?
无:根据定义,等于Y
的数字X
不能大于Y
。
只要 32 位浮点数是整数值,将其转换为 64 位整数就不会丢失精度。但是如果原来的浮点数不是整数,就会失去精度。
当然这个比较可能是真的。 1.0f == float(1ull)
这个问题只有在关于哪些 64 位整数可以表示为浮点数而不损失精度时才有意义。这也是第一段所说的,第一个要点相当混乱。并且显然有相当多的数字对于这个属性是正确的(任何大于 2^32 但小于 2^64 的 2 的幂)。
重要的检查是这个。如果取原来的 64 位整数值,将其转换为浮点数,然后将该浮点数转换回整数,并得到原始值,则可以传输浮点数代替整数;你可以确定对方可以恢复原来的整数(因为你刚刚自己测试过)。
【参考方案1】:
确实有一些数字是这样的:
2^33 可以完美地表示为浮点数,但显然不能表示为 32 位整数。以下代码应按预期工作:
bool representable_as_float(int64_t value)
float repr = value;
return repr >= -0x1.0p63 && repr < 0x1.0p63 && (int64_t)repr == value;
重要的是要注意,虽然我们基本上是在做 (int64_t)(float)value 而不是相反 - 如果转换为 float 失去任何精度,我们会感兴趣。
检查 repr 是否小于 int64_t 的最大值很重要,因为否则我们可以调用未定义的行为,因为强制转换为 float 可能会舍入到下一个更高的数字(然后可能大于最大值可能在 int64_t 中)。 (感谢@tmyklebu 指出这一点)。
两个样本:
// powers of 2 can easily be represented
assert(representable_as_float(((int64_t)1) << 33));
// Other numbers not so much:
assert(!representable_as_float(std::numeric_limits<int64_t>::max()));
【讨论】:
你不是通过(int64_t)(float)0x7fffffffffffffffLL
来获得UB吗?这里对float的转换向上取整,所以对int64_t的转换会溢出,也就是UB。
@tmyklebu 公平点。从 int -> float 的转换总是安全的(afaics?),但另一个不是。这使整个事情变得更加有趣。
嗯...所以,这会导致 UB,是吗?
我认为你需要在转换为int64_t
之前检查是否repr >= -0x1.0p63 && repr < 0x1.0p63
。【参考方案2】:
以下内容基于Julia's method for comparing floats and integers。这不需要访问 80 位 long double
s 或浮点异常,并且应该在任何舍入模式下工作。我相信这应该适用于任何 C float
类型(IEEE754 与否),并且不会导致任何未定义的行为。
更新:从技术上讲,这假定二进制 float
格式,并且 float
指数大小足以表示 264:对于标准 IEEE754 binary32(您在您的问题中引用),但不是二进制16。
#include <stdio.h>
#include <stdint.h>
int cmp_flt_uint64(float x,uint64_t y)
return (x == (float)y) && (x != 0x1p64f) && ((uint64_t)x == y);
int main()
float x = 0x1p64f;
uint64_t y = 0xffffffffffffffff;
if (cmp_flt_uint64(x,y))
printf("true\n");
else
printf("false\n");
;
这里的逻辑如下:
只有当x
是区间 [0,264] 中的非负整数时,第一个等式才成立。
第二个检查x
(因此(float)y
)不是264:如果是这种情况,那么y
不能用float
精确表示,并且所以比较是错误的。
x
的任何剩余值都可以精确转换为 uint64_t
,因此我们进行转换和比较。
【讨论】:
【参考方案3】:不,您需要在长双精度尾数可以容纳 63 位的架构上比较 (long double)x == (long double)y
。这是因为一些大的 long long int 在您将它们转换为浮点数时会丢失精度,并与非等效浮点数进行比较,但如果您转换为 long double,它不会在该架构上丢失精度。
以下程序演示了在 x86 上使用 gcc -std=c99 -mssse3 -mfpmath=sse
编译时的这种行为,因为这些设置使用了足够宽的 long double,但防止在计算中隐式使用更高精度的类型:
#include <assert.h>
#include <stdint.h>
const int64_t x = (1ULL<<62) - 1ULL;
const float y = (float)(1ULL<<62);
// The mantissa is not wide enough to store
// 63 bits of precision.
int main(void)
assert ((float)x == (float)y);
assert ((long double)x != (long double)y);
return 0;
编辑:如果您没有足够宽的长双打,以下方法可能会起作用:
feclearexcept(FE_ALL_EXCEPT);
x == y;
ftestexcept(FE_INEXACT);
我认为,虽然我可能弄错了,但实现可能会在转换过程中以失去精度的方式舍入 x。
另一个可行的策略是比较
extern uint64_t x;
extern float y;
const float z = (float)x;
y == z && (uint64_t)z == x;
这应该会捕获由于舍入误差导致的精度损失,但如果转换为 z 舍入,它可能会导致未定义的行为。如果在将 x 转换为 z 时将转换设置为向零舍入,它将起作用。
【讨论】:
我不知道为什么我被否决了,但也许演示这种行为的代码示例会改变你的想法? 我没有投反对票——我实际上对新版本投了赞成票——但是是的,这个例子让你的回答变得有价值。在没有long double
类型的情况下比较整数和浮点值有一些技巧,可以准确地表示每种原始类型的所有值。 gynvael.coldwind.pl/?id=535twitter.com/spun_off/status/467929922259144704
您在底部的建议检查了ftrunc(x) == x
,但x
在您的示例中是一个整数(即使假设x
和y
的角色颠倒了,我也不确定它是否有效,例如float x = 0x1.0p62
、int64_t y = 0x3fffffffffffffff
)。
哎呀,底部的那个位是另一个问题的答案。
@Lorehead 在所谓的“往返策略”中,(uint64_t)z
可能会导致未定义的行为,例如对于x
设置为0xffffffffffffffff
导致z
为0x1.0p64
。以上是关于比较 uint64_t 和 float 的数值等价性的主要内容,如果未能解决你的问题,请参考以下文章
uint8_t / uint16_t / uint32_t /uint64_t 这些数据类型是什么?
uint8_t / uint16_t / uint32_t /uint64_t 这些数据类型是什么?
如何在 ARM 上的 uint64_t 和 poly64_t 之间进行转换?