如何测试无损双/整数转换?
How to test for lossless double / integer conversion?
我有一张双人床,还有一张 int64_t。我想知道它们是否具有完全相同的值,以及将一种类型转换为另一种类型是否不会丢失任何信息。
我当前的实现如下:
int int64EqualsDouble(int64_t i, double d) {
return (d >= INT64_MIN)
&& (d < INT64_MAX)
&& (round(d) == d)
&& (i == (int64_t)d);
}
我的问题是:这个实现是否正确?如果不是,正确答案是什么? 正确答案必须是没有假阳性,也没有假阴性。
一些示例输入:
- int64EqualsDouble(0, 0.0) 应该 return 1
- int64EqualsDouble(1, 1.0) 应该 return 1
- int64EqualsDouble(0x3FFFFFFFFFFFFFFF, (double)0x3FFFFFFFFFFFFFFFF) 应该 return 0,因为 2^62 - 1 可以用 int64_t 精确表示,但不能用 double.
- int64EqualsDouble(0x4000000000000000, (double)0x4000000000000000) 应该 return 1,因为 2^62 可以在 int64_t 和 double.
中精确表示
- int64EqualsDouble(INT64_MAX, (double)INT64_MAX) 应该return 0,因为INT64_MAX不能精确表示为double
- int64EqualsDouble(..., 1.0e100) 应该 return 0,因为 1.0e100 不能精确表示为 int64_t.
是的,您的解决方案可以正常工作,因为它被设计为这样做,因为 int64_t
在使用类似于二进制 IEEE 754 的平台上按定义 (C99 7.18.1.1:1) 表示为二进制补码double
类型的双精度。和this one.
基本一样
在这些条件下:
d < INT64_MAX
是正确的,因为它等同于 d < (double) INT64_MAX
并且在转换为双精度时,数字 INT64_MAX
,等于 0x7ffffffffffffffff,四舍五入。因此,您希望 d
严格小于结果 double
以避免在执行 (int64_t)d
.
时触发 UB
另一方面,INT64_MIN
是 -0x8000000000000000,是可精确表示的,这意味着等于 (double)INT64_MIN
的 double
可以等于一些 int64_t
并且不应被排除(这样的 double
可以转换为 int64_t
而不会触发未定义的行为)
不言而喻,由于我们专门针对整数和二进制浮点数使用了 2 的补码假设,因此在不同的平台上,这种推理并不能保证代码的正确性。以具有二进制 64 位浮点数和 64 位 1 的补码整数类型 T
的平台为例。在那个平台上 T_MIN
是 -0x7fffffffffffffff
。该数字向下舍入为 double
,结果为 -0x1.0p63
。在该平台上,按照编写的方式使用您的程序,将 -0x1.0p63
用于 d
会使前三个条件为真,从而导致 (T)d
中出现未定义的行为,因为 overflow in the conversion from integer to floating-point is undefined behavior.
如果您可以访问完整的 IEEE 754 功能,则有一个更短的解决方案:
#include <fenv.h>
…
#pragma STDC FENV_ACCESS ON
feclearexcept(FE_INEXACT), f == i && !fetestexcept(FE_INEXACT)
此解决方案利用从整数到浮点数的转换设置 INEXACT 标志 iff 转换不精确(也就是说,如果 i
不能完全表示为 double
)。
INEXACT 标志保持未设置且 f
等于 (double)i
当且仅当 f
和 i
在各自的类型中表示相同的数学值。
此方法要求编译器在代码访问 FPU 状态时收到警告,通常使用 #pragma STDC FENV_ACCESS on
但这通常不受支持,您必须改用编译标志。
OP 的代码有一个可以避免的依赖。
为了比较成功,d
必须是整数,round(d) == d
负责处理。即使 d
,因为 NaN 也会失败。
d
必须 在数学上 在 [INT64_MIN
... INT64_MAX
] 范围内并且如果 if
条件适当确保,然后最终 i == (int64_t)d
完成测试。
所以问题归结为比较 INT64
限制与 double
d
。
让我们假设 FLT_RADIX == 2
,但不一定 IEEE 754 binary64。
d >= INT64_MIN
不是问题,因为 -INT64_MIN
是 2 的幂并且完全转换为相同值的 double
,因此 >=
是准确的。
代码想做数学计算 d <= INT64_MAX
,但这可能行不通,所以出现问题。 INT64_MAX
是 "power of 2 - 1" 并且 可能 无法准确转换 - 这取决于 double
的精度是否超过 63 位 - 呈现比较不清楚。一种解决方案是将比较减半。 d/2
没有精度损失,INT64_MAX/2 + 1
精确转换为 double
2 的幂
d/2 < (INT64_MAX/2 + 1)
[编辑]
// or simply
d < ((double)(INT64_MAX/2 + 1))*2
因此,如果代码不想依赖精度低于 uint64_t
的 double
。 (可能适用于 long double
的东西)更便携的解决方案是
int int64EqualsDouble(int64_t i, double d) {
return (d >= INT64_MIN)
&& (d < ((double)(INT64_MAX/2 + 1))*2) // (d/2 < (INT64_MAX/2 + 1))
&& (round(d) == d)
&& (i == (int64_t)d);
}
注意:没有舍入模式问题。
[编辑]更深层次的限制解释
数学上的保证,INT64_MIN <= d <= INT64_MAX
,可以重新表述为 INT64_MIN <= d < (INT64_MAX + 1)
,因为我们处理的是整数。由于 (double) (INT64_MAX + 1)
在代码中的原始应用肯定是 0,因此替代方案是 ((double)(INT64_MAX/2 + 1))*2
。对于具有 double
更高 2 次幂到 ((double)(INT64_MAX/FLT_RADIX + 1))*FLT_RADIX
的稀有机器,这可以扩展。比较限制是 exact 的 2 次幂,转换为 double
不会造成精度损失,并且 (lo_limit >= d) && (d < hi_limit)
是精确的,无论浮点数的精度如何。注意:带有 FLT_RADIX == 10
的罕见浮点数仍然是一个问题。
除了 Pascal Cuoq 的详尽回答,并考虑到您在评论中提供的额外上下文,我还要添加一个负零测试。你应该保留负零,除非你有充分的理由不这样做。您需要进行特定测试以避免将它们转换为 (int64_t)0
。根据您当前的提议,负零将通过您的测试,存储为 int64_t
并作为正零读回。
我不确定测试它们的最有效方法是什么,也许是这样:
int int64EqualsDouble(int64_t i, double d) {
return (d >= INT64_MIN)
&& (d < INT64_MAX)
&& (round(d) == d)
&& (i == (int64_t)d
&& (!signbit(d) || d != 0.0);
}
我有一张双人床,还有一张 int64_t。我想知道它们是否具有完全相同的值,以及将一种类型转换为另一种类型是否不会丢失任何信息。
我当前的实现如下:
int int64EqualsDouble(int64_t i, double d) {
return (d >= INT64_MIN)
&& (d < INT64_MAX)
&& (round(d) == d)
&& (i == (int64_t)d);
}
我的问题是:这个实现是否正确?如果不是,正确答案是什么? 正确答案必须是没有假阳性,也没有假阴性。
一些示例输入:
- int64EqualsDouble(0, 0.0) 应该 return 1
- int64EqualsDouble(1, 1.0) 应该 return 1
- int64EqualsDouble(0x3FFFFFFFFFFFFFFF, (double)0x3FFFFFFFFFFFFFFFF) 应该 return 0,因为 2^62 - 1 可以用 int64_t 精确表示,但不能用 double.
- int64EqualsDouble(0x4000000000000000, (double)0x4000000000000000) 应该 return 1,因为 2^62 可以在 int64_t 和 double. 中精确表示
- int64EqualsDouble(INT64_MAX, (double)INT64_MAX) 应该return 0,因为INT64_MAX不能精确表示为double
- int64EqualsDouble(..., 1.0e100) 应该 return 0,因为 1.0e100 不能精确表示为 int64_t.
是的,您的解决方案可以正常工作,因为它被设计为这样做,因为 int64_t
在使用类似于二进制 IEEE 754 的平台上按定义 (C99 7.18.1.1:1) 表示为二进制补码double
类型的双精度。和this one.
在这些条件下:
d < INT64_MAX
是正确的,因为它等同于d < (double) INT64_MAX
并且在转换为双精度时,数字INT64_MAX
,等于 0x7ffffffffffffffff,四舍五入。因此,您希望d
严格小于结果double
以避免在执行(int64_t)d
. 时触发 UB
另一方面,
INT64_MIN
是 -0x8000000000000000,是可精确表示的,这意味着等于(double)INT64_MIN
的double
可以等于一些int64_t
并且不应被排除(这样的double
可以转换为int64_t
而不会触发未定义的行为)
不言而喻,由于我们专门针对整数和二进制浮点数使用了 2 的补码假设,因此在不同的平台上,这种推理并不能保证代码的正确性。以具有二进制 64 位浮点数和 64 位 1 的补码整数类型 T
的平台为例。在那个平台上 T_MIN
是 -0x7fffffffffffffff
。该数字向下舍入为 double
,结果为 -0x1.0p63
。在该平台上,按照编写的方式使用您的程序,将 -0x1.0p63
用于 d
会使前三个条件为真,从而导致 (T)d
中出现未定义的行为,因为 overflow in the conversion from integer to floating-point is undefined behavior.
如果您可以访问完整的 IEEE 754 功能,则有一个更短的解决方案:
#include <fenv.h>
…
#pragma STDC FENV_ACCESS ON
feclearexcept(FE_INEXACT), f == i && !fetestexcept(FE_INEXACT)
此解决方案利用从整数到浮点数的转换设置 INEXACT 标志 iff 转换不精确(也就是说,如果 i
不能完全表示为 double
)。
INEXACT 标志保持未设置且 f
等于 (double)i
当且仅当 f
和 i
在各自的类型中表示相同的数学值。
此方法要求编译器在代码访问 FPU 状态时收到警告,通常使用 #pragma STDC FENV_ACCESS on
但这通常不受支持,您必须改用编译标志。
OP 的代码有一个可以避免的依赖。
为了比较成功,d
必须是整数,round(d) == d
负责处理。即使 d
,因为 NaN 也会失败。
d
必须 在数学上 在 [INT64_MIN
... INT64_MAX
] 范围内并且如果 if
条件适当确保,然后最终 i == (int64_t)d
完成测试。
所以问题归结为比较 INT64
限制与 double
d
。
让我们假设 FLT_RADIX == 2
,但不一定 IEEE 754 binary64。
d >= INT64_MIN
不是问题,因为 -INT64_MIN
是 2 的幂并且完全转换为相同值的 double
,因此 >=
是准确的。
代码想做数学计算 d <= INT64_MAX
,但这可能行不通,所以出现问题。 INT64_MAX
是 "power of 2 - 1" 并且 可能 无法准确转换 - 这取决于 double
的精度是否超过 63 位 - 呈现比较不清楚。一种解决方案是将比较减半。 d/2
没有精度损失,INT64_MAX/2 + 1
精确转换为 double
2 的幂
d/2 < (INT64_MAX/2 + 1)
[编辑]
// or simply
d < ((double)(INT64_MAX/2 + 1))*2
因此,如果代码不想依赖精度低于 uint64_t
的 double
。 (可能适用于 long double
的东西)更便携的解决方案是
int int64EqualsDouble(int64_t i, double d) {
return (d >= INT64_MIN)
&& (d < ((double)(INT64_MAX/2 + 1))*2) // (d/2 < (INT64_MAX/2 + 1))
&& (round(d) == d)
&& (i == (int64_t)d);
}
注意:没有舍入模式问题。
[编辑]更深层次的限制解释
数学上的保证,INT64_MIN <= d <= INT64_MAX
,可以重新表述为 INT64_MIN <= d < (INT64_MAX + 1)
,因为我们处理的是整数。由于 (double) (INT64_MAX + 1)
在代码中的原始应用肯定是 0,因此替代方案是 ((double)(INT64_MAX/2 + 1))*2
。对于具有 double
更高 2 次幂到 ((double)(INT64_MAX/FLT_RADIX + 1))*FLT_RADIX
的稀有机器,这可以扩展。比较限制是 exact 的 2 次幂,转换为 double
不会造成精度损失,并且 (lo_limit >= d) && (d < hi_limit)
是精确的,无论浮点数的精度如何。注意:带有 FLT_RADIX == 10
的罕见浮点数仍然是一个问题。
除了 Pascal Cuoq 的详尽回答,并考虑到您在评论中提供的额外上下文,我还要添加一个负零测试。你应该保留负零,除非你有充分的理由不这样做。您需要进行特定测试以避免将它们转换为 (int64_t)0
。根据您当前的提议,负零将通过您的测试,存储为 int64_t
并作为正零读回。
我不确定测试它们的最有效方法是什么,也许是这样:
int int64EqualsDouble(int64_t i, double d) {
return (d >= INT64_MIN)
&& (d < INT64_MAX)
&& (round(d) == d)
&& (i == (int64_t)d
&& (!signbit(d) || d != 0.0);
}