我们正在将应用程序从 .NET Framework 4.8 更新到 .NET 8。
在回归测试期间,我们注意到隐式扩展转换似乎以不同的顺序发生,导致结果发生一些变化。
在 .NET Framework 中,类似这样的操作似乎d1= f1 * f2
会先将f1
和转换f2
为双精度数,然后再执行乘法,而在 .NET 8 中,则会先进行浮点数之间的乘法,然后再进行扩展。
我知道浮点二进制数学的行为。我并不是想说其中一个是“错误的”——只是想了解为什么行为发生了变化。
并且:有没有办法将.NET 8 行为暂时改回 .NET Framework 行为,以便我们更好地理解我们的回归测试?
(PS:是的,我知道如果我们一开始没有隐式转换的话这不会是个问题。但这是一个庞大的遗留代码库,我无法轻易改变它。)
控制台应用程序测试代码:
Console.WriteLine(".NET 8");
Console.WriteLine("Input Float: 0.3333333F");
float f1 = 0.3333333F;
Console.WriteLine("Decimal: " + f1.ToString());
Console.WriteLine("Binary: " + GetFloatBinary(f1));
Console.WriteLine();
Console.WriteLine("Float multiplication first, then conversion");
Console.WriteLine("f2 = f1 * f1");
float f2 = f1 * f1;
Console.WriteLine("Decimal: " + f2.ToString());
Console.WriteLine("Binary: " + GetFloatBinary(f2));
Console.WriteLine("d1 = (double)f2");
double d1 = (double)f2;
Console.WriteLine("Decimal: " + d1.ToString());
Console.WriteLine("Binary: " + GetDoubleBinary(d1));
Console.WriteLine();
Console.WriteLine("Conversion first, multiplication second");
Console.WriteLine("d2 = (double)f1 * (double)f1");
double d2 = (double)f1 * (double)f1;
Console.WriteLine("Decimal: " + d2.ToString());
Console.WriteLine("Binary: " + GetDoubleBinary(d2));
Console.WriteLine();
Console.WriteLine("Let the platform decide");
Console.WriteLine("d3 = f1 * f1");
double d3 = f1 * f1;
Console.WriteLine("Decimal: " + d3.ToString());
Console.WriteLine("Binary: " + GetDoubleBinary(d3));
Console.WriteLine();
Console.ReadLine();
static string GetFloatBinary(float value)
{
const int bitCount = sizeof(float) * 8;
int intValue = System.BitConverter.ToInt32(BitConverter.GetBytes(value), 0);
return Convert.ToString(intValue, 2).PadLeft(bitCount, '0');
}
static string GetDoubleBinary(double value)
{
const int bitCount = sizeof(double) * 8;
int intValue = System.BitConverter.ToInt32(BitConverter.GetBytes(value), 0);
return Convert.ToString(intValue, 2).PadLeft(bitCount, '0');
}
结果:
.NET FRAMEWORK
Input Float: 0.3333333F
Decimal: 0.3333333
Binary: 00111110101010101010101010101010
Float multiplication first, then conversion
f2 = f1 * f1
Decimal: 0.1111111
Binary: 00111101111000111000111000110111
d1 = (double)f2
Decimal: 0.111111097037792
Binary: 0000000000000000000000000000000011100000000000000000000000000000
Conversion first, multiplication second
d2 = (double)f1 * (double)f1
Decimal: 0.111111097865635
Binary: 0000000000000000000000000000000011100011100011100011100100000000
Let the platform decide
d3 = f1 * f1
Decimal: 0.111111097865635
Binary: 0000000000000000000000000000000011100011100011100011100100000000
.NET 8
Input Float: 0.3333333F
Decimal: 0.3333333
Binary: 00111110101010101010101010101010
Float multiplication first, then conversion
f2 = f1 * f1
Decimal: 0.1111111
Binary: 00111101111000111000111000110111
d1 = (double)f2
Decimal: 0.1111110970377922
Binary: 0000000000000000000000000000000011100000000000000000000000000000
Conversion first, multiplication second
d2 = (double)f1 * (double)f1
Decimal: 0.11111109786563489
Binary: 0000000000000000000000000000000011100011100011100011100100000000
Let the platform decide
d3 = f1 * f1
Decimal: 0.1111110970377922
Binary: 0000000000000000000000000000000011100000000000000000000000000000
我认为我清楚地了解了发生了什么变化,但我找不到任何关于原因的文献。
谢谢您的见解!
仅对我而言,该问题在 32 位版本(或 .NET Framework 上首选 32 位的 AnyCPU)上可重现。
两者的 IL 是相同的,并且 ECMA 附录中没有提及,因此推测在这方面没有真正的变化。
在 64 位上,.NET Framework 与我的情况下的 .NET 6 相同(并且结果与问题相同) - 浮点数首先相乘。
在 32 位上,.NET Framework 的反汇编如下:
关于
fld
指令:但这种不一致性与 ECMA 335 标准一致:
I.12.1.3 浮点数据类型的处理:
github 上的这个问题讨论了跨运行时实现(不同的 JITters 发出不同的指令)的这种变化,特别是那些评论(我在 [] 中澄清):
还有这些问题/答案更详细地讨论了该主题:
这篇博文很好地总结了主要问题(JIT 编译器在哪个实现中使用哪个指令集),并提供了可能的解决方案(使用
decimal
或第三方库):