浮点数会离开[0,1]范围的操作有哪些?

7

我经常使用范围为 [0,1] 的 floatdouble 类型。我知道浮点数运算是不精确的,所以在进行操作之前/之后,我通常会将我的值夹紧(clamp)到这个范围内,以确保它们处于此范围。

在某些情况下,我依赖于浮点数不会稍微偏向负数,并且恰好等于 <= 1,因此这一步是必要的。

例如,在以下任何一个函数中,这都是必要的:

// x and y are guaranteed to be in [0, 1]
float avg(float x, float y) {
    // the average of [0, 1] values should always be in [0, 1]
    return std::clamp<float>((x + y) / 2, 0, 1);
}

float mul(float x, float y) {
    // the product of [0, 1] values should always be in [0, 1]
    return std::clamp<float>(x * y, 0, 1);
}

float pow(float x, unsigned y) {
    // raising an [0, 1] value to any unsigned power should also result in an [0, 1] value
    return std::clamp<float>(std::pow(x, y), 0, 1);
}

当浮点数进行算术运算超出 [0, 1] 范围时,有哪些一致的规则吗?


我想你知道在精确算术中,以上任何一项都不需要夹紧器,所以问题是不准确是否会改变这一点? - 463035818_is_not_a_number
floor、ceil、round、abs、sign和*可能是唯一保持在范围内的算术运算。对于其他所有操作,您必须推断出操作的组合仅在[0,1]范围内产生结果,就像您的avg函数一样。+肯定会超出范围,但/会将其带回。 - Goswin von Brederlow
1
@idclev463035818 是的,这就是重点。如果浮点数在数学意义上是实数,那么这一切都不必要。但是浮点数并不是实数,也许在计算平均值或其他事情时会破坏这个[0,1]的要求。 - Jan Schultke
@GoswinvonBrederlow:sqrt和我认为cbrt也进入了IEEE754的特殊函数列表。 - Bathsheba
2
关于“浮点运算不精确”的问题——实际上并非如此。问题在于浮点数不是实数,而且很多基于多年经验的直觉都不适用。 - Pete Becker
2个回答

6
限制这个回答只针对IEEE754标准。 0、1和2都可以被精确地表示为一个“float”类型。算术运算符需要返回最好的浮点数值。由于x和y都不大于1,所以它们的和不能大于2,否则将存在更好的“float”类型来表示这个和。换句话说,两个略小于1的“float”类型的和不能大于2。同样适用于乘法。
第三个操作需要使用夹具,因为不能保证“std::pow(x, y)”返回最佳的“float”类型。

对于 (a + b + c),最佳值可能大于 1,那么 (a + b + c) / 3 也可能大于 1。因此,我认为你必须针对每个函数进行推理,没有任何广泛的规则可遵循。 - Goswin von Brederlow
1
@idclev463035818:抱歉,那个补充回答来得有点晚了。 - Bathsheba
2
我非常确定(a+b+c+...)不能大于N,其中N个数字<=1。我没有一个干净的证明。而且它可能需要N < 2^24。 - Rick James
@aka.nice - 从代数上讲,这应该是微不足道的,但请记住,pow(x, y)的一个老式但有效的实现是exp(y log x)。因此,你的不等式论证并不一定成立。 - Bathsheba
@Bathsheba 如果log和exp是弱单调的,那么这个老式的pow实现也应该是弱单调的,不是吗?对于x=1,log显然为0,我会惊讶于对于x<1的一些实现会回答非负的log。exp(0)显然为1,我会惊讶于exp(x)>1对于x<0...同样在0周围,exp()不会是负数... - aka.nice
显示剩余4条评论

1
角落问题
float pow(float x, unsigned y) {
    // raising an [0, 1] value to any unsigned power should also result in an [0, 1] value
    return std::clamp<float>(std::pow(x, y), 0, 1);
}

使用std::pow(±0, 0)可能导致超出[0..1]范围(或导致域错误),因为该结果不是由std::pow()或数学上解决的:零次幂

如果遵循IEEE,则结果为1,但不需要符合库遵循该标准。

如果不遵循IEEE,并且std::pow(0,0)返回NAN,则我希望std::clamp(NAN,0,1)也会返回NAN并破坏系统。

备选替代代码

return (y==0) ? 1 : x;

注意,函数的结果可以是-0.0,但该值仍在[0...1]范围内。
我对mul()avg()没有任何关于-0.0的担忧,中间计算以更高的精度和不同的舍入模式完成。

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