比较uint64_t和float的数值等价性

Comparing uint64_t and float for numeric equivalence

本文关键字:float uint64 比较      更新时间:2023-10-16

我正在编写一个协议,它使用RFC 7049作为其二进制表示。该标准规定,如果数字的数值与相应的64位数字相等,则协议可以使用32位浮点数表示数字。转换不能导致精度的损失。

  • 哪些32位浮点数可以大于64位整数,并且与它们在数字上相等?
  • 比较float x; uint64_t y; (float)x == (float)y是否足以确保值是相等的?这种比较会正确吗?

RFC 7049§3.6。数字

在本规范中,所有数字表示对于相同的数值是相等的。这意味着编码器可以将0.0的浮点值编码为整数0。然而,它也意味着应用程序期望找到整数值只能找到浮点值,如果编码器决定这些是需要的,例如当浮点值是比64位整数更紧凑。

当然有一些数字是这样的:

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)值,而不是反过来——我们感兴趣的是转换为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())); 

下面是基于Julia比较浮点数和整数的方法。这不需要访问80位long double或浮点异常,并且应该在任何舍入模式下工作。我相信这应该适用于任何C float类型(IEEE754与否),并且不会导致任何未定义的行为。

更新:从技术上讲,这假设了二进制float格式,并且float指数大小足以表示264:对于标准IEEE754 binary32(您在问题中提到的)来说,这当然是正确的,但不是,说,binary16。

#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("truen");
  else 
    printf("falsen");
  ;
}

这里的逻辑如下:

  • x是区间[0,264]内的非负整数时,第一个等式为真。
  • 第二个检查x(因此(float)y)不是264:如果是这种情况,则y不能由float精确表示,因此比较为假。
  • x的任何剩余值都可以准确地转换为uint64_t,因此我们可以进行强制转换和比较。

不,您需要在长双精度体尾数可以容纳63位的体系结构上比较(long double)x == (long double)y。这是因为当将一些大的long long int型转换为float型并与不相等的float型进行比较时,它们会失去精度,但如果转换为long double型,则在该结构上不会失去精度。

下面的程序演示了在x86上使用gcc -std=c99 -mssse3 -mfpmath=sse编译时的这种行为,因为这些设置使用足够宽的长双精度类型,但防止在计算中隐式使用更高精度的类型:

#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;
}

Edit:如果你没有足够宽的长双精度,下面的可能工作:

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时将转换设置为向零四舍五入,则可以工作。