为什么当我使用变量时,带有连分数的模数运算符的结果会有所不同,并且会因不同的编译器而不同?
Why does the result of modulus operator with continued fractions differ when I use variables and differ for different compilers?
我有一些代码会根据我使用的编译器产生不同的结果:
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 中使用此代码为 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。例如:
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;
我有一些代码会根据我使用的编译器产生不同的结果:
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 中使用此代码为 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。例如:
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;