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

本文关键字:int 无效 浮点数 转换 | 更新日期: 2023-09-27 18:11:19

我写下面的代码:

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

假设发票总计 = 36000。那么vat必须是 2880 而是 2879!我将代码更改为

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

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

我想知道()是否具有更高的优先级! 而且浮点数精确 2880.0 不少一点,所以不能四舍五入!

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

float具有一些未显示的"隐藏"精度。尝试观看invoice.total.ToString("R"),您可能会发现它并不完全36000

或者,这可能是运行时为中间结果选择"更宽"的存储位置(如 64 位或 80 位 CPU 寄存器或类似位置(的结果invoice.total * 0.08f

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

(int)(invoice.total * 0.08f)

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

floatfloat(原文如此!(的额外强制转换看起来像是无操作,但它确实迫使运行时舍入并丢弃不需要的精度。这是很少有记录的。[将提供参考。您可能想要阅读的相关线程:浮点数在 C# 中是否一致?可以吗?


你的例子实际上是原型,所以我决定更详细一点。这些东西在IEEE 754实现之间的差异一节中有很好的描述,该部分是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(对于二进制浮点数,将子表达式转换为临时变量可能会改变结果。(2( 使用二进制浮点数,C# 编译器设置如 x86 vs. x64可能会改变结果。


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

>0.08f不能完全表示。最接近的单精度值为

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

哪些输出

288028792880

为什么您的两个变体表现不同?因为编译器选择存储中间值,以便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));

这导致

288028792880

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

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

哪些输出

2880

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

只是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在最终对话转换为 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 指令提供的精度,这很可能会超过浮点格式的精度。因此,由于乘法结果的浮点转换,第一个代码会导致额外的精度损失。第二个代码不会受到这种额外的精度损失的影响,因为它避免了中间浮点转换。

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

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