简单浮点计算出错

Art*_*zar 4 c# floating-point

我遇到了麻烦:

int q = 150;
float s = 0.7f;
float z = q*s;
int z1 = (int) (q*s);
int z2 = (int) z;
Run Code Online (Sandbox Code Playgroud)

这导致了

  • z1int与价值104
  • z2int与价值105

有谁能解释一下?我不明白这些结果.


为了避免关闭,我(RenéVogt)添加了这些信息:

  • q*s得到一个float105.0f(或者可能104.999999,但字符串表示最终为105).
  • 所以zfloat105

现在的问题是,为什么会(int)z产生结果105,但(int)(q*s)结果104呢?我可以在我的机器上重现这个(i7,Win10,VS2015,.NET4.6.1)


和IL代码:

// Initialisation
// q = 150
ldc.i4 0x96  
stloc.0
// s = 0.7f
ldc.r4 0.69999999 
stloc.1

// calculating z 
ldloc.0 // load q
conv.r4 // convert to float
ldloc.1 // load s
mul     // q*s
stloc.2 // store float result to z

// calulating z1
ldloc.0 // load q
conv.r4 // convert to float
ldloc.1 // load s
mul     // q*s
conv.i4 // convert to int
stloc.3 // store int result in z1 => 104!

// calculating z2
ldloc.2 // loading float z
conv.i4 // converting to int
stloc.s // write to z2 (last local variable -> "s" as stack address)
        // => 105
Run Code Online (Sandbox Code Playgroud)

所以我认为z1和之间的唯一区别z2是,z2中间float结果从寄存器写入局部变量的(z)存储位置.但是这会对结果产生怎样的影响呢?

Equ*_*lsk 6

数字0.7不能用a精确表示float,而是值s更接近0.699999988079071044921875.
所述int的值q将被转换为一个float,因为这可以表示直接它保持为150.

如果你将两者相乘,你就不会105完全得到:

q = 150
s = 0.699999988079071044921875
q*s = 104.999998211861

现在参考CLI规范(ECMA-335) §12.1.3中的相关部分:

当内部表示具有比其标称类型更大的范围和/或精度的浮点值被放入存储位置时,它将自动强制转换为存储位置的类型.这可能涉及精度损失或创建超出范围的值(NaN,+无穷大或无穷大).但是,如果在未经修改的情况下从存储位置重新加载该值,则该值可能会保留在内部表示中以供将来使用.编译器有责任确保保留值在后续加载时仍然有效,同时考虑到别名和其他执行线程的影响(参见内存模型(第12.6节)).但是,在执行显式转换(conv.r4或conv.r8)之后,不允许进行额外精度的自由,此时内部表示必须在关联类型中准确表示.

因此q * s产生的精度高于float可处理的精度值.将此直接存储到int:

var z1 = (int)(q * s);
Run Code Online (Sandbox Code Playgroud)

该值永远不会强制转换为类型float,而是直接转换int为104,从而截断为104.

在所有其他示例中,该值被float转换为或存储在a中,因此转换为最接近的可能float值,即105.