我在下面写下代码:
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并不是一点点,所以不能进行舍入!
答案 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)
从float
到float
(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
时,作为乘法算法的一部分,我们还必须再次舍入以产生最接近精确的乘积表示(实际)因子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)对于二进制浮点,将一个子表达式赋予temp变量可能会改变结果。 (2)对于二进制浮点,x86
与x64
之类的C#编译器设置可能会改变结果。
当然,正如每个人到处说的那样,不要将float
或double
用于货币申请;在那里使用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.0
和ldloc.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 转换的方式。