我有一些代码会根据我使用的编译器产生不同的结果:
float a = 1f;
float b = 6f;
float result = a % (a / b);
float result1 = 1f % (1f / 6f);
Console.WriteLine("{0} , {1}", result, result1);
使用 Roslyn 运行此代码时,输出为:
5.551115E-17,0.1666666
当我在 Unity for Windows (Mono) 中使用此代码构建项目时,我得到了
0 , 0.1666666
Unity 上的同一项目,适用于 Android (il2cpp):
0.1666666,0.1666666
与 .NET 4.7.2 相同:
5.551115E-17,5.551115E-17
与 .NET 5 相同:
0.16666664,0.16666664
首先,Mono 的区别似乎在于它实际上优化了整个浮点算术(请注意,模运算符之前的值和分数的分子是相同的,并且立即用零替换整个表达式,这是有道理的从逻辑上讲,但破坏了整个“我明确输入这些都是浮点数”的事情)。 但为什么会有所有其他差异,尤其是第一个差异?该行为是否在某处定义(例如,其中一个编译器以“正确的方式”执行此操作,而其他编译器则没有)?
基本上,IL 没有针对
float
与 double
的正确“堆栈类型”。它只有 F
(ECMA 335 I.12.1.3)
对于
float a = 1f; float b = 6f; float result = a % (a / b);
,C# 编译器本质上会发出:
ldc.r4 1
ldc.r4 6
stloc.0
dup
ldloc.0
div
rem
由于运行时与其单一类型的工作方式意味着可以自由地将其视为:
float a = 1f; float b = 6f;
double tmp1 = (a / b);
double tmp2 = a % tmp1;
float result = (float)tmp2;
这样对待的话,结果总是
5.551115E-17
。
这在很大程度上只是传统 32 位 JIT 的一个问题,因为它必须使用 x87 FPU 堆栈,而由于更改舍入模式的成本过高,该堆栈只能以 64 位执行所有操作。由于遗留原因,它也没有插入中间转换以在操作之间浮动。
RyuJIT(现代 64 位 JIT,自 2.1 起所有 .NET Core 使用的 JIT,IIRC)使用 x86 SIMD 指令 (SSE/SSE2),而不是本机支持 32 位和 64 位操作,因此它不没有这个问题。所有操作都直接作为“正确”类型完成(请注意,可能仍然存在一些我不记得的边缘情况)。
使这一点在各处保持一致的“修复”是在每个“操作”之后插入显式强制转换以浮动。例如:
float a = 1f; float b = 6f;
float result = a % (float)(a / b);
同样:
float a = 1f; float b = 6f;
float result = a / b;
result = a % result;