为什么分割结果因铸件类型而异?(跟进)

本文关键字:跟进 类型 结果 分割 为什么 | 更新日期: 2023-09-27 18:20:49

这是这个问题的后续问题:为什么除法结果因铸造类型而异?

快速摘要:

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。那么,为什么铸造结果会有所不同呢?

为什么分割结果因铸件类型而异?(跟进)

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

当将包含 64 / 0.8f 结果的float转换为运行时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(因为双精度包含足够的精度来导致问题,此处(。
但是,使用 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 完成计算时不会发生舍入误差(。在强制转换为双精度之前显式浮动(被 ReSharper 标记为冗余,顺便说一句("解决了"这个问题。但是,当计算在编译时完成时(仅在使用常量时可能(,显式强制转换为float似乎被忽略/优化掉。

TLDR:浮点计算比最初看起来要复杂得多。

C# 语言规范允许以大于类型精度的精度计算中间浮点结果。这很可能是这里正在发生的事情。

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

(从浮点到浮点的转换是最近的 - 从技术上讲,它们是根据 FPU 的舍入模式完成的,但 C# 不允许将 FPU 的舍入模式从其"到最近"默认值更改。从浮点类型到整数类型的转换将被截断。

尽管 C# 遵循 Java(恕我直言不幸的是(的领导,每当将指定为 double 的东西存储到float时都需要显式强制转换,但 C# 编译器生成的代码允许 .NET 运行时double执行计算,并在表达式类型应该的许多上下文中使用这些double值, 根据语言规则,float .

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

如果将表达式编写为 (byte)(float)(sixtyFour / 0.8f) ,则应强制结果在截断小数部分之前四舍五入到最接近的可表示float值。 尽管强制转换为float可能看起来是多余的(表达式的编译时类型已经是float(,但强制转换会将"应该是float但实际上double的东西"变成实际上是float的东西。

从历史上看,某些语言会指定所有浮点运算都在类型 double 上执行; float的存在不是为了加快计算速度,而是为了减少存储需求。 通常不需要指定常量作为类型float,因为除以 0.8000000000000000044(double值 0.8(并不比除以 0.800000011920929(值0.8f(慢。 C#有点烦人地不允许float1 = float2 / 0.8;因为"精度损失",而是偏爱不太精确的float1 = float2 / 0.8f;,甚至不介意可能错误的double1 = float1 / 0.8f;。 在float值之间执行操作的事实并不意味着结果实际上是一个float - 它只是意味着编译器将允许它在某些上下文中静默地舍入为float,但在其他上下文中不会强制它。