【问题标题】:casting a float number into int number will result to invalid int将浮点数转换为 int 数将导致 int 无效
【发布时间】:2014-07-03 08:42:23
【问题描述】:

我写下面的代码:

 int vat = (int)(invoice.total * 0.08f);

假设 invoice.total = 36000。那么vat 必须是 2880,但是是 2879! 我将代码更改为

float v = invoice.total * 0.08f;
int vat = (int)v;

现在vat 具有正确的值 (2880)。

不知道() 有没有更高的优先级!而且 float 也正好是 2880.0 不少于一点,所以不能进行四舍五入!

【问题讨论】:

  • 这是不可重现的。 ideone.com/NnuB2e
  • 我也能复现……(用36000还是36000f好像都无所谓……)
  • 嗯,它是可重现的,令人惊讶的是:ideone.com/USiCEX
  • total 属性/字段(在invoice 中)是float 类型还是int 类型(或类似类型)?

标签: c# math floating-point int type-conversion


【解决方案1】:

float 拥有一些未显示的“隐藏”精度。试试看invoice.total.ToString("R"),你可能会发现它不完全是36000

或者,这可能是由于您的运行时为中间结果invoice.total * 0.08f 选择了“更广泛”的存储位置,例如 64 位或 80 位 CPU 寄存器或类似位置。

编辑:您可以通过更改来消除运行时选择过宽的存储位置所产生的影响

(int)(invoice.total * 0.08f)

进入

(int)(float)(invoice.total * 0.08f)

floatfloat(原文如此!)的额外演员表看起来像是无操作,但它确实会强制运行时舍入并丢弃不需要的精度。这没有很好的记录。 [将提供参考。]您可能想阅读的相关主题:Are floating-point numbers consistent in C#? Can they be?


你的例子实际上是典型的,所以我决定更详细一点。这些东西在Differences Among IEEE 754 Implementations 部分中有很好的描述,该部分是作为 David Goldberg 的What Every Computer Scientist Should Know About Floating-Point Arithmetic 的附录(由匿名作者编写)。所以假设我们有这样的代码:

static int SO_24548957_I()
{
  float t = 36000f; // exactly representable
  float r = 0.08f;  // this is not representable, rounded

  float temporary = t * r;
  int v = (int)temporary;

  return v; // always(?) 2880
}

一切似乎都很好,但我们决定将临时变量重构掉,所以我们写道:

static int SO_24548957_II()
{
  float t = 36000f; // exactly representable
  float r = 0.08f;  // this is not representable, rounded

  int v = (int)(t * r);

  return v; // could be 2880 or 2879 depending on strange things
}

然后砰!我们程序的行为发生了变化。如果您为平台x86(或Any CPU 选择Prefer 32-bit)编译,您可以在大多数系统上看到更改(至少在我的系统上!)。优化与否(发布或调试模式)在理论上可能是相关的,硬件架构当然也很重要。

对于许多人来说,2880 和 2879 在符合 IEEE-754 的系统上都可以是正确答案,这让很多人感到非常惊讶,但请阅读我提供的链接。

为了详细说明“不可表示”的含义,让我们看看 C# 编译器在遇到符号 0.08f 时必须做什么。由于float(32 位二进制浮点)的工作方式,我们将不得不在以下两者之间进行选择:

10737418 / 2**27  ==  0.079 999 998 2...

10737419 / 2**27  ==  0.080 000 005 6...

其中** 表示求幂(即“幂”)。由于第一个更接近所需的数学值,我们必须选择那个。所以实际值比期望值小一点。现在,当我们进行乘法并希望再次存储在Single时,作为乘法算法的一部分,我们还必须再次舍入以产生最接近精确“数学”的乘积表示" (实际)因子 360000.0799999982... 的乘积。在这种情况下,您 很幸运,最接近的 Single 实际上是 2880,因此在我们的例子中,乘法过程涉及到该值的四舍五入。

因此上面的第一个代码示例给出了2880

但是,在上面的第二个代码示例中,乘法可能会在一些处理许多位(通常为 64 或 80)的 CPU 硬件中完成(在运行时的选择中,我们无法真正帮助)。在这种情况下,任何两个 32 位浮点数的乘积,就像我们的一样,可以在不需要对最终结果进行四舍五入的情况下计算出来,因为 64 位或 80 位足以容纳两个 32 位浮点数的完整乘积。很明显这个产品小于2880,因为 0.0799999982... 小于 0.08

因此上面的第二个方法示例可以返回2879

为了比较,这段代码:

static int SO_24548957_III()
{
  float t = 36000f; // exactly representable
  float r = 0.08f;  // this is not representable, rounded

  double temporary = t * (double)r;
  int v = (int)temporary;

  return v; // always(?) 2879
}

总是给出2879,因为我们明确告诉编译器将Single 转换为Double,这意味着添加一堆二进制零,所以我们肯定会得到2879 的情况。

经验教训: (1) 对于二进制浮点,将子表达式分解为临时变量可能会改变结果。 (2) 对于二进制浮点,x86x64 之类的 C# 编译器设置可能会改变结果。


当然,正如大家到处说的那样,不要将floatdouble 用于货币应用程序;在那里使用decimal

【讨论】:

  • 36000 完全可以表示
  • @DavidHeffernan 这是肯定的,但是像36000.0039f35999.9961f 这样的数字具有与36000f 不同的表示形式,但以通常的格式打印时两者看起来相同。因此,仅仅因为一个数字 看起来 像 36,000(完全可以表示),它可能是一个不同的数字。但是,因为如果他使用临时变量,输出会发生变化,所以我现在已经删除了我的答案的那部分。
  • 问题是,如果 invoice.total 是一个值为 36000 的 int,那么行为就像问题中所述。但你说的当然也可能是真的。只是不需要解释输出。
【解决方案2】:

0.08f 不能准确表示。 closest single precision value

0.07999999821186065673828125

所以你实际计算

36000 * 0.07999999821186065673828125

仅比2880 少一点。然后截断该值,从而接收到值2879

这可能是您第一次遇到这样的问题,但我敢打赌您没想到0.08f 的实际值会是0.07999999821186065673828125

考虑这个变体:

float f = 36000 * 0.08f;
Console.WriteLine((int)f);
double d1 = 36000 * 0.08f;
Console.WriteLine((int)d1);
double d2 = 36000 * 0.08d;
Console.WriteLine((int)d2);

哪个输出

2880 2879 2880

为什么您的两个变体表现不同?因为编译器选择将invoice.total * 0.08f 的中间值存储为非单精度。


显然你在这里玩火。这种行为完全取决于浮点运算的基本属性。您选择 binary 浮点不可避免地会导致这样的问题。解决此问题的一种方法是将值四舍五入为最接近的整数。

float f = 36000 * 0.08f;
Console.WriteLine((int)Math.Round(f));
double d1 = 36000 * 0.08f;
Console.WriteLine((int)Math.Round(d1));
double d2 = 36000 * 0.08d;
Console.WriteLine((int)Math.Round(d2));

导致

2880 2879 2880

您也可以考虑使用Decimal 进行此类计算。这样您就可以对十进制而不是二进制表示进行操作,因此将能够准确地表示所有这些值。

int vat = (int)(36000 * 0.08m);
Console.WriteLine(vat);

哪个输出

2880

具体如何解决这个问题,很大程度上取决于计算的细节和你的业务逻辑。但根本问题是二进制浮点不能准确地表示您的计算。

【讨论】:

  • and: (int) 将截断浮点数,而不是舍入它。然而,这不是问题。两个语句都应该产生相同的值,不管是不是 2880
  • 从技术上讲,从 C# 的角度来看,在第一个版本中,invoice.total * 0.08f 表达式 不是 如您所声称的 double。它是两个浮点数/单数的乘积,因此本身就是一个浮点数。 补充: 不同之处在于运行时选择将float(32 位)中间值“保持”在“精度过高”的存储位置。结果不需要是2879
  • @David Hefferman 如果 invoice.total 是十进制,则 invoice.total * 0.08f 不会在 C# 中编译而无需显式转换。如果 invoice.total 是双倍的,它会编译并且在技术上是双倍的,但是在将 0.08f 写为浮点数而不是双倍时已经丢失了一些精度。但在这种情况下,invoice.total 不能为 double,因为它在没有显式转换的情况下分配给 float v,如果 double 则无法完成。
  • 好吧,你让我明白了。一定如 Jeppe 所说。
【解决方案3】:

只是 Jeppe 和 David 关于编译器选择不同精度的中间值的答案的附录。

你的第一个表达式,写成这样的函数:

static int Calc1(int value)
{
    float v = value * 0.08f;
    return (int) v;
}

将产生以下 IL 代码:

.method private hidebysig static int32  Calc1(int32 'value') cil managed
{
    // Code size       12 (0xc)
    .maxstack  2
    .locals init ([0] float32 v)
    IL_0000:  ldarg.0
    IL_0001:  conv.r4
    IL_0002:  ldc.r4     7.9999998e-002
    IL_0007:  mul
    IL_0008:  stloc.0
    IL_0009:  ldloc.0
    IL_000a:  conv.i4
    IL_000b:  ret
} // end of method Program::Calc1

注意,指令stloc.0ldloc.0 将乘法结果转换为float,然后最终对话为int (conv.i4) .

现在让我们看看你的第二个表达式:

static int Calc2(int value)
{
    return (int)(value * 0.08f);
}

以及相应的 IL 代码:

.method private hidebysig static int32  Calc2(int32 'value') cil managed
{
    // Code size       10 (0xa)
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  conv.r4
    IL_0002:  ldc.r4     7.9999998e-002
    IL_0007:  mul
    IL_0008:  conv.i4
    IL_0009:  ret
} // end of method Program::Calc2

注意,乘法的结果直接转换成一个int

乘法结果具有由 JIT 编译器选择的浮点 CPU 指令提供的精度,这很可能会超过 float 格式的精度。因此,由于乘法结果的 float 转换,第一个代码会导致额外的精度损失。第二个代码不会遭受这种额外的精度损失,因为它避免了中间 float 转换。

(实际上,对于第一个代码示例,JIT 编译器可能足够聪明,可以指示 CPU 仅使用单精度进行浮点运算,因此已经使用低单精度进行乘法运算。)

您可能会争辩说,第一个示例的 IL cod 中的 stloc.0 ldloc.0 组合毫无意义,如果编译器足够聪明,则应该将其优化掉。唉,事实并非如此。再次查看第一个示例的 C# 代码。在那里,源代码明确要求必须将乘法结果转换为 float 值(通过变量 v)。 stloc.0 ldloc.0 组合只是编译器选择遵守这种要求的 float 转换的方式。

【讨论】:

    猜你喜欢
    • 2014-09-03
    • 2014-05-09
    • 1970-01-01
    • 1970-01-01
    • 2022-08-02
    • 2021-01-05
    • 1970-01-01
    • 1970-01-01
    • 2012-05-09
    相关资源
    最近更新 更多