为什么将 double 转换为 double 会发出 conv.r8 IL 指令

Why casting double to double emits conv.r8 IL instruction

当从 double -> double 进行 casting 时,C# 编译器是否有任何理由发出 conv.r8

这看起来完全没有必要(从 int -> int、char -> char 等进行转换)不会发出等效的转换指令(正如您在为 I2I() 方法生成的 IL 中看到的那样)。

class Foo
{
    double D2D(double d) => (double) d;
    int I2I(int i) => (int) i;
}

导致 IL:

.class private auto ansi '<Module>'
{
} // end of class <Module>

.class private auto ansi beforefieldinit Foo
    extends [System.Private.CoreLib]System.Object
{
    // Methods
    .method private hidebysig 
        instance float64 D2D (
            float64 d
        ) cil managed 
    {
        // Method begins at RVA 0x2050
        // Code size 3 (0x3)
        .maxstack 8

        IL_0000: ldarg.1
        IL_0001: conv.r8
        IL_0002: ret
    } // end of method Foo::D2D

    .method private hidebysig 
        instance int32 I2I (
            int32 i
        ) cil managed 
    {
        // Method begins at RVA 0x2054
        // Code size 2 (0x2)
        .maxstack 8

        IL_0000: ldarg.1
        IL_0001: ret
    } // end of method Foo::I2I

    .method public hidebysig specialname rtspecialname 
        instance void .ctor () cil managed 
    {
        // Method begins at RVA 0x2057
        // Code size 8 (0x8)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: call instance void [System.Private.CoreLib]System.Object::.ctor()
        IL_0006: nop
        IL_0007: ret
    } // end of method Foo::.ctor

} // end of class Foo

You can also play with the code above.

简短版本是 CLI 中 double/float 的中间表示有意未指定。因此,编译器将始终发出从 doubledouble(或 floatfloat)的显式转换,以防它会改变表达式的含义。

在这种情况下并没有改变意思,但是编译器不知道。 (虽然 JIT 会做,并且会优化它。)


如果你想要所有粗糙的背景细节...

下面的 ECMA-335 引用特别来自具有 Microsoft 特定实施说明的版本,can be downloaded from here。 (请注意,由于我们谈论的是 IL,所以我将从 .NET 运行时的虚拟机的角度而不是从任何特定的处理器体系结构的角度来谈论。)

可以在 CodeGenerator.EmitIdentityConversion:

中找到 Roslyn 发出这个看似不必要的指令的理由

An explicit identity conversion from double to double or float to float on non-constants must stay as a conversion. An implicit identity conversion can be optimized away. Why? Because (double)d1 + d2 has different semantics than d1 + d2. The former rounds off to 64 bit precision; the latter is permitted to use higher precision math if d1 is enregistered.

(强调和格式化我的。)

这里要注意的重要一点是“允许使用更高精度的数学”。要理解为什么会这样,我们需要了解 运行time 如何在低级别表示不同的类型。 .NET 运行时使用的虚拟机是基于堆栈的,所有中间值都进入所谓的评估堆栈。 (不要与处理器的调用堆栈混淆,它可能会或可能不会用于 运行 时间计算堆栈上的东西。)

分区 I §12.3.2.1 评估堆栈(第 88 页) 描述了评估堆栈,并列出了可以在堆栈上表示的内容:

While the CLI, in general, supports the full set of types described in §12.1, the CLI treats the evaluation stack in a special way. While some JIT compilers might track the types on the stack in more detail, the CLI only requires that values be one of:

  • int64, an 8-byte signed integer
  • int32, a 4-byte signed integer
  • native int, a signed integer of either 4 or 8 bytes, whichever is more convenient for the target architecture
  • F, a floating point value (float32, float64, or other representation supported by the underlying hardware)
  • &, a managed pointer
  • O, an object reference
  • *, a “transient pointer,” which can be used only within the body of a single method, that points to a value known to be in unmanaged memory (see the CIL Instruction Set specification for more details. * types are generated internally within the CLI; they are not created by the user).
  • A user-defined value type

值得注意的是,唯一的浮点类型是 F 类型,您会注意到它故意含糊不清,并不代表特定的精度。 (这样做是为了为 运行 时间实现提供灵活性,因为它们必须 运行 在许多不同的处理器上,这些处理器可能会或可能不会更喜欢浮点运算的特定精度级别。)

如果我们再深入一点,分区 I §12.1.3 浮点数据类型的处理(第 79 页):[=62= 中也提到了这一点]

Storage locations for floating-point numbers (statics, array elements, and fields of classes) are of fixed size. The supported storage sizes are float32 and float64. Everywhere else (on the evaluation stack, as arguments, as return types, and as local variables) floating-point numbers are represented using an internal floating-point type.

对于最后一块拼图,我们需要了解conv.r8的确切定义,定义在Partiion III §3.27 conv.<to type> - 数据转换(pg 68):

conv.r8: Convert to float64, pushing F on stack.

最后,将 F 转换为 F 的细节在 Partition III §1.5 Table 8: Conversion Operations (pg 20):(释义)

If input (from the evaluation stack) is F and convert-to is "All float types": Change precision³

³Converts from the current precision available on the evaluation stack to the precision specified by the instruction. If the stack has more precision than the output size the conversion is performed using the IEC 60559:1989 “round-to-nearest” mode to compute the low order bit of the result.

所以在这种情况下,您应该将 conv.r8 理解为“从未指定的浮点格式转换为 double”而不是“从 double 转换为 double” . (虽然在这种情况下,我们可以非常确定计算堆栈上的 F 已经是 double 精度,因为它来自 double 参数。)


总而言之:

  • .NET 运行时有一个 float64 类型,但仅用于存储目的。
  • 出于评估目的(和传递参数),必须改用未指定精度的 F 类型。
  • 这意味着有时“不必要的”显式转换为 double 实际上会改变表达式的精度。
  • C# 编译器不知道它是否重要,所以它总是发出从 Ffloat64 的转换。 (但是 JIT 确实如此,在这种情况下,将在 运行 时间优化演员表。)