在C语言中将double截断为float

5
这是一个非常简单的问题,但却非常重要,因为它会极大地影响我的整个项目。假设我有以下代码片段:
unsigned int x = 0xffffffff;
float f = (float)((double)x * (double)2.328306436538696e-010); //  x/2^32

我期望f是类似于0.99999这样的数字,但实际上它被四舍五入成了1,因为它是最接近的float近似值。这不好,因为我需要在[0,1)区间内使用float值,而不是[0,1]。我相信这是一个简单的问题,但我需要一些帮助。

5个回答

8

在C语言中(自C99起),你可以通过libm中的fesetround函数来改变舍入方向。

#include <stdio.h>
#include <fenv.h>
int main()
{
    #pragma STDC FENV_ACCESS ON
    fesetround(FE_DOWNWARD);
    // volatile -- uncomment for GNU gcc and whoever else doesn't support FENV
    unsigned long x = 0xffffffff;
    float f = (float)((double)x * (double)2.328306436538696e-010); //  x/2^32
    printf("%.50f\n", f);
}

已经使用IBM XL、Sun Studio、clang和GNU gcc进行测试。在所有情况下,这给了我0.99999994039535522460937500000000000000000000000000


这是一个C++11函数吗? - Mark B
@MarkB C99函数,包含在C++11中。 - Cubbi
这段代码是否可以在转换后立即将舍入方向设置回去? - Patricia Shanahan
@PatriciaShanahan 当然,我只是展示了一个最小的例子。 - Cubbi

3
当将double转换为float时,在默认IEEE 754舍入模式下,向上舍入为1或更高的值是0x1.ffffffp-1(使用C99十六进制表示法,因为您的问题标记为“C”)。
你有以下选择:
  1. 在转换之前将FPU舍入模式设置为向下舍入
  2. 乘以(0x1.ffffffp-1 / 0xffffffffp0)(给定或取一个ULP),以利用完整的单精度范围[0,1),而不会得到1.0f的值。
方法2 leads to use the constant 0x1.ffffff01fffffp-33:
double factor = nextafter(0x1.ffffffp-1 / 0xffffffffp0, 0.0);
unsigned int x = 0xffffffff;
float f = (float)((double)x * factor);
printf("factor:%a\nunrounded:%a\nresult:%a\n", factor, (double)x * factor, f);

输出:

factor:0x1.ffffff01fffffp-33
unrounded:0x1.fffffefffffffp-1
result:0x1.fffffep-1

1

我能做的不多 - 你的int只有32位,但是float的尾数只有24位。四舍五入是必然发生的。你可以将处理器的舍入模式改为向下舍入而不是向最近舍入,但这会导致一些副作用,特别是如果你完成后不恢复舍入模式。

你使用的公式没有问题,它为给定输入产生了最准确的答案。只是有一个边缘情况未能满足硬性要求。测试特定的边缘情况并将其替换为最接近满足要求的值没有问题:

if (f >= 1.0f)
    f = 0.99999994f;

0.999999940395355224609375是IEEE-754浮点数在不等于1.0的情况下可以取得的最接近值。


1
这不是一个有帮助的答案。正如其他答案所示(并且它们已经展示了如何做到),有一些你可以做的事情。 - Eric Postpischil
@EricPostpischil,为什么这不是有用的呢?它提供了一个可行的解决方案,而不会使舍入模式保持不变,从而改变所有中间和后续计算。 - Mark Ransom
“很少有事情可以做”这个说法是误导性的,没有必要让人感到气馁。有关intfloat中比特的声明不相关; OP并不希望得到完全的映射。他们不是要求避免四舍五入,只是要控制它。 - Eric Postpischil
@EricPostpischil,我断言会有舍入问题,因为问题定义使其无法避免,并且我给出了我的理由。您可以更改舍入的性质,但您无法避免它。我的答案是唯一处理边缘情况并在所有其他情况下保持最高可能精度的答案。 - Mark Ransom

1
你可以将值截断到最大精度(保留24个高位),然后除以2^24,这样就可以得到最接近的浮点数值,而不会被四舍五入为1。
unsigned int i = 0xffffffff;
float value = (float)(i>>8)/(1<<24);

printf("%.20f\n", value);
printf("%a\n", value);

>>> 0.99999994039535522461
>>> 0x1.fffffep-1

如果向零舍入每个值(而不仅仅是接近1的值)适合OP,那么这可能是一个好方法。示例中的hack是不必要的;我们可以使用“%a”格式说明符以展示浮点数的组成方式。 - Eric Postpischil
@EricPostpischil 感谢您介绍 %a 格式,我之前不知道这个。 - Joachim Isaksson

0
我的最终解决方案是缩小常数乘数的大小。这可能是最好的解决方案,因为无论如何都没有必要乘以双精度浮点数。在转换为浮点数后,精度也不会受到影响。
因此,将 2.328306436538696e-010 更改为 2.3283063

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接