【问题标题】:Properly subtracting float values正确减去浮点值
【发布时间】:2012-07-23 03:56:15
【问题描述】:

我正在尝试创建一个值数组。这些值应该是“2.4,1.6,.8,0”。我每一步都减去 0.8。

这就是我的做法(代码 sn-p):

float mean = [[_scalesDictionary objectForKey:@"M1"] floatValue];  //3.2f
float sD = [[_scalesDictionary objectForKey:@"SD1"] floatValue];   //0.8f

nextRegion = mean;
hitWall = NO;
NSMutableArray *minusRegion = [NSMutableArray array];


while (!hitWall) {

    nextRegion -= sD;

if(nextRegion<0.0f){
    nextRegion = 0.0f;
    hitWall = YES;
}

[minusRegion addObject:[NSNumber numberWithFloat:nextRegion]];

}

我得到这个输出:

minusRegion = (
    "2.4",
    "1.6",
    "0.8000001",
    "1.192093e-07",
    0
)

我不希望 0.8 和 0 之间的数字非常小。是否有截断这些值的标准方法?

【问题讨论】:

  • 我的意思是截断平均值和标准差,所以它们不会给我非常小的值。
  • 我编辑了我的答案,以展示使用 floorf 函数执行此操作的简单方法
  • 我很困惑为什么 0.8 的浮点表示的尾数是 1.100110011001...(重复)而不是精确的 1.100100000000。 (.8 以浮点数表示时存储为 1.6 * 2^-1。选择的 1.6 在不太重要的位中有一些古怪,这导致问题中出现错误)
  • 我认为发生了一些奇怪的事情。我主要关心的是找到一个实用的解决方案,但我也有兴趣了解官方原因/解释。

标签: objective-c ios c floating-point


【解决方案1】:

3.2 和 .8 都不能完全表示为 32 位浮点数。最接近 3.2 的可表示数字是 3.2000000476837158203125(十六进制浮点数,0x1.99999ap+1)。最接近 0.8 的可表示数字是 0.800000011920928955078125 (0x1.99999ap-1)。

当从 3.2000000476837158203125 中减去 0.800000011920928955078125 时,精确的数学结果是 2.400000035762786865234375 (0x1.3333338p+1)。此结果也不能完全表示为 32 位浮点数。 (您可以在十六进制浮点数中轻松看到这一点。一个 32 位浮点数有一个 24 位有效数。“1.3333338”在“1”中有一位,中间六位中有 24 位,在”8”。)所以结果被四舍五入到最接近的 32 位浮点数,即 2.400000095367431640625 (0x1.333334p+1)。

从中减去 0.800000011920928955078125 得到 1.6000001430511474609375 (0x1.99999cp+0),这是完全可表示的。 (“1”是一位,五个9是20位,“c”有两个有效位。“c”中的低位两位是尾随零,可以忽略。所以有23个有效位.)

从中减去 0.800000011920928955078125 得到 0.800000131130218505859375 (0x1.99999ep-1),这也是完全可表示的。

最后,从中减去 0.800000011920928955078125 得到 1.1920928955078125e-07 (0x1p-23)。

这里要吸取的教训是浮点并不代表所有数字,它会将结果四舍五入,以便为您提供它可以代表的最接近的数字。在编写使用浮点运算的软件时,您必须理解并允许这些舍入操作。实现这一点的一种方法是使用您知道可以表示的数字。其他人建议使用整数算术。另一种选择是主要使用您知道可以用浮点数精确表示的值,其中包括最大为 224 的整数。所以你可以从 32 开始减去 8,得到 24,然后是 16,然后是 8,然后是 0。这些将是你用于循环控制和继续计算而没有错误的中间值。当您准备好交付结果时,您可以除以 10,产生接近 3.2、2.4、1.6、0.8 和 0 的数字(精确)。这样,您的算法将只在每个结果中引入一个舍入误差,而不是在一次迭代中累积舍入误差。

【讨论】:

    【解决方案2】:

    您正在查看旧的浮点舍入错误。幸运的是,在您的情况下,它应该很容易处理。只需夹紧:

    if( val < increment ){
        val = 0.0;
    }
    

    虽然,作为Eric Postpischil explained below

    以这种方式钳制是一个坏主意,因为有时舍入会导致迭代变量比增量略小而不是略多,并且这种钳制将有效地跳过迭代。例如,如果初始值为 3.6f(而不是 3.2f),步长是 0.9f(而不是 0.8f),那么每次迭代中的值将略低于 3.6、2.7、1.8 和 0.9。此时,钳位会将略低于 0.9 的值转换为零,并跳过迭代。

    因此在进行比较时可能需要减去少量。

    您应该考虑的一个更好的选择是使用整数而不是浮点数进行计算,然后再转换。

    int increment = 8;
    int val = 32;
    
    while( val > 0 ){
        val -= increment;
    
        float new_float_val = val / 10.0;
    };
    

    【讨论】:

    • 注释以供将来参考:10.0 中的“.0”在执行除法时确实很重要。
    • 是的,这是类型提升:some_int / 10 会产生 int,它不会产生小数值。除以文字 float 首先“提升”int,得到 float 结果。
    • 以这种方式钳制是一个坏主意,因为有时舍入会导致迭代变量比增量略小而不是略多,并且这种钳制将有效地跳过迭代。例如,如果初始值为 3.6f(而不是 3.2f),步长是 0.9f(而不是 0.8f),那么每次迭代中的值将略低于 3.6、2.7、1.8 和 0.9。此时,钳位会将略低于 0.9 的值转换为零,并跳过迭代。
    • @Eric:我考虑在比较if( val &lt; (increment - buffer) ) 中添加一个“epsilon”,但考虑到问题的简单性,我认为它是多余的。无论如何,我认为整数解决方案是可取的。不过,很好地解释了这个问题。
    【解决方案3】:

    另一种方法是将减法得到的数字乘以 10,然后转换为整数,然后将该整数除以 10.0。

    您可以像这样使用 floor 函数 (floorf) 轻松做到这一点:

    float newValue = floorf(oldVlaue*10)/10;

    【讨论】:

    • 有趣。也许我没有使用正确的数据类型(浮点数)。
    • 不,我认为浮动应该没问题。
    猜你喜欢
    • 1970-01-01
    • 2016-05-02
    • 1970-01-01
    • 2022-01-10
    • 2018-09-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多