为什么除法结果会根据演员类型而有所不同? (跟进)

时间:2014-09-06 19:17:37

标签: c# floating-point

这是此问题的后续内容:Why does a division result differ based on the cast type?

快速摘要:

byte b1 = (byte)(64 / 0.8f); // b1 is 79
int b2 = (int)(64 / 0.8f); // b2 is 79
float fl = (64 / 0.8f); // fl is 80

问题是:为什么结果会根据演员类型而有所不同?在找到答案的时候,我遇到了一个我无法解释的问题。

var bytes = BitConverter.GetBytes(64 / 0.8f).Reverse(); // Reverse endianness
var bits = bytes.Select(b => Convert.ToString(b, 2).PadLeft(8, '0'));
Console.WriteLine(string.Join(" ", bits));

这输出以下内容:

01000010 10100000 00000000 00000000

以IEEE 754格式分解:

0 10000101 01000000000000000000000

星座:

0 => Positive

指数:

10000101 => 133 in base 10

尾数:

01000000000000000000000 => 0*2^-1 + 1*2^-2 + 0*2^-3 ... = 1/4 = 0.25

十进制表示:

(1 + 0.25) * 2^(133 - 127) (Subtract single precision bias)

这恰好导致80.那么为什么投射结果有所不同?

3 个答案:

答案 0 :(得分:4)

我在另一个帖子中的答案并不完全正确:实际上,在运行时计算时,(byte)(64 / 0.8f) 为80

在运行时将包含float结果的64 / 0.8f转换为byte时,结果实际为80.但是,当转换为完成时,情况并非如此。作业的一部分:

float f1 = (64 / 0.8f);

byte b1 = (byte) f1;
byte b2 = (byte)(64 / 0.8f);

Console.WriteLine(b1); //80
Console.WriteLine(b2); //79

当b1包含预期结果时,b2关闭。根据反汇编,b2分配如下:

mov         dword ptr [ebp-48h],4Fh 

因此,编译器似乎在运行时计算结果的不同结果。但是,我不知道这是否是预期的行为。

编辑:也许是Pascal Cuoq所描述的效果:在编译期间,C#编译器使用double来计算表达式。这导致79,xxx被截断为79(这里的double包含足够的精度以引起问题) 然而,使用float,我们实际上并没有遇到问题,因为浮点数"错误"不在浮动范围内。

在运行期间,这个也打印79:

double d1 = (64 / 0.8f);
byte b3 = (byte) d1;
Console.WriteLine(b3); //79

EDIT2:根据Pascal Cuoq的要求,我运行了以下代码:

int sixtyfour = Int32.Parse("64");
byte b4 = (byte)(sixtyfour / 0.8f);
Console.WriteLine(b4); //79

结果是79.所以上面说的编译器和运行时计算不同的结果是不正确的。

EDIT3 :将之​​前的代码更改为(再次归功于Pascal Cuoq)时,结果为80:

byte b5 = (byte)(float)(sixtyfour / 0.8f);
Console.WriteLine(b5); //80

但请注意,写作时并非如此(结果为79):

byte b6 = (byte)(float)(64 / 0.8f);
Console.WriteLine(b6); //79

所以这里似乎正在发生的事情:(byte)(64 / 0.8f)未被评估为float,而是评估为double(在将其转换为byte之前)。这会导致舍入误差(使用float进行计算时不会发生)。在转换为double之前浮动的显式转换(由ReSharper,BTW标记为冗余)"解决"这个问题。但是,当计算在编译期间完成时(仅在使用常量时可能),显式转换为float似乎被忽略/优化了。

TLDR:浮点计算比最初看起来更复杂。

答案 1 :(得分:3)

C#语言规范允许to compute intermediate floating-point results at a precision greater than that of the type。这很有可能发生在这里。

虽然计算到更高精度的64 / 0.8略低于80(因为0.8无法在二进制浮点中精确表示),并且在截断为整数类型时转换为79,如果结果除法转换为float,它舍入为80.0f

(从浮点到浮点的转换是最接近技术上的,它们是根据FPU的舍入模式完成的,但C#不允许将FPU的舍入模式从“更改为”到最近“default。从浮点到整数类型的转换截断。”

答案 2 :(得分:0)

即使C#遵循Java(恕我直言,不幸)领导要求显式转换,任何时候将指定为double的内容存储到float,C#编译器生成的代码允许.NET运行时以double执行计算,并在许多上下文中使用这些double值,根据语言规则,表达式的类型应为float

幸运的是,C#编译器确实提供了至少一种方法来确保应该舍入到最接近的可表示float的事物实际上是:将它们显式地转换为float

如果您将表达式编写为(byte)(float)(sixtyFour / 0.8f),则应该在截断小数部分之前强制将结果四舍五入到最接近的可表示float值。虽然对float的强制转换可能看似多余(表达式的编译时类型已经是float),但强制转换将变为“应该是float的东西但是真的{ {1}}“进入实际为double的内容。

从历史上看,某些语言会指定所有浮点运算都在类型float上执行; double存在不是为了加速计算,而是为了减少存储需求。通常不需要将常量指定为类型float,因为除以0.800000000000000044(float值0.8)并不慢于除以0.800000011920929(值double)。 C#有点令人讨厌,因为“精度损失”而不允许0.8f,而是偏向不太精确的float1 = float2 / 0.8;,甚至不介意可能错误的float1 = float2 / 0.8f;。虽然操作是在double1 = float1 / 0.8f;值之间执行的,但这并不意味着结果实际上是float - 它只是意味着编译器将允许它静默舍入为float在某些情况下,但不会在其他情况下强迫它。