将浮点数转换为 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 不少一点,所以不能四舍五入!
float
具有一些未显示的"隐藏"精度。尝试观看invoice.total.ToString("R")
,您可能会发现它并不完全36000
。
或者,这可能是运行时为中间结果选择"更宽"的存储位置(如 64 位或 80 位 CPU 寄存器或类似位置(的结果invoice.total * 0.08f
。
编辑:您可以通过更改来摆脱运行时选择太宽的存储位置所产生的影响
(int)(invoice.total * 0.08f)
到
(int)(float)(invoice.total * 0.08f)
从 float
到 float
(原文如此!(的额外强制转换看起来像是无操作,但它确实迫使运行时舍入并丢弃不需要的精度。这是很少有记录的。[将提供参考。您可能想要阅读的相关线程:浮点数在 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
中时,作为乘法算法的一部分,我们还必须再次舍入以产生最接近(实际(因子36000
和0.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
可能会改变结果。
当然,正如每个人到处都说的那样,不要将float
或double
用于货币应用;在那里使用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.0
和ldloc.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
组合只是编译器选择遵守这种要求的浮点转换的方式。