将一个浮点数转换为int数将导致int无效

时间:2014-07-03 08:42:23

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

我在下面写下代码:

 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)。

我想知道()是否有更多优先权!并且浮动确切地说是2880.0并不是一点点,所以不能进行舍入!

3 个答案:

答案 0 :(得分:2)

A 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(sic!)的额外演员看起来像一个无操作,但它确实迫使运行时变圆并抛弃那些不必要的精度。记录很少。 [将提供参考。]您可能想要阅读的相关主题:Are floating-point numbers consistent in C#? Can they be?


你的例子实际上是典型的,所以我决定更详细一点。这个内容在Differences Among IEEE 754 Implementations部分有详细描述,该部分是作为附录(由匿名作者撰写)写给David Goldberg的每个计算机科学家应该知道的关于浮点运算的内容。所以假设我们有这个代码:

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)对于二进制浮点,将一个子表达式赋予temp变量可能会改变结果。 (2)对于二进制浮点,x86x64之类的C#编译器设置可能会改变结果。


当然,正如每个人到处说的那样,不要将floatdouble用于货币申请;在那里使用decimal

答案 1 :(得分:1)

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的中间值存储到除单个精度之外的精度。


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

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

究竟如何解决问题在很大程度上取决于计算的细节和业务逻辑。但基本问题是二进制浮点不能完全代表你的计算。

答案 2 :(得分:0)

只是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 ({{ 1}}}发生。

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

conv.i4

和相应的IL代码:

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

请注意,乘法的结果直接转换为 int

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

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

您可能想要争论第一个示例的IL cod中的.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 stloc.0组合是没有意义的,如果编译器足够聪明,应该优化掉。唉,事实并非如此。再看一下第一个例子的C#代码。在那里,源代码明确要求乘法结果必须转换为 float 值(通过变量 v )。 ldloc.0 stloc.0组合仅仅是编译器选择遵循这种所需的 float 转换的方式。