为什么模运算符会根据所使用的编译器和变量返回不同的结果?

问题描述 投票:0回答:1

我有一些代码会根据我使用的编译器产生不同的结果:

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 的区别似乎在于它实际上优化了整个浮点算术(请注意,模运算符之前的值和分数的分子是相同的,并且立即用零替换整个表达式,这是有道理的从逻辑上讲,但破坏了整个“我明确输入这些都是浮点数”的事情)。 但为什么会有所有其他差异,尤其是第一个差异?该行为是否在某处定义(例如,其中一个编译器以“正确的方式”执行此操作,而其他编译器则没有)?

c# compilation floating-point roslyn
1个回答
4
投票

基本上,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;
© www.soinside.com 2019 - 2024. All rights reserved.