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

Vag*_*aus 4 c# cil compilation intermediate-language

C# 编译器在从进行转换时是否有任何理由发出conv.r8double -> double

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

class Foo
{
    double D2D(double d) => (double) d;
    int I2I(int i) => (int) i;
}
Run Code Online (Sandbox Code Playgroud)

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

Run Code Online (Sandbox Code Playgroud)

您还可以使用上面的代码

Pat*_*vid 7

简而言之,CLI 中double/的中间表示是故意未指定的。float因此,编译器总是会发出从doubleto double(或floatto float)的显式转换,以防它改变表达式的含义。

\n

在这种情况下它不会改变含义,但编译器不知道这一点。(JIT 确实会优化它。)

\n
\n

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

\n

下面的 ECMA-335 参考资料具体来自带有 Microsoft 特定实施说明的版本,可以从此处下载。(请注意,由于我们正在讨论 IL,因此我将从 .NET 运行时虚拟机的角度进行讨论,而不是从任何特定的处理器架构进行讨论。)

\n

Roslyn 发出这个看似不必要的指令的理由可以在以下位置找到CodeGenerator.EmitIdentityConversion

\n
\n

从到或到\n非常量的显式标识转换必须保留为转换。可以优化隐身份转换。\n 为什么?因为与 具有不同的语义。\n前者四舍五入为 64 位精度;后者可以使用更高\n精度的数学,如果doubledoublefloatfloat(double)d1 + d2d1 + d2d1

\n
\n

(强调并格式化我的。)

\n

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

\n

分区 I \xc2\xa712.3.2.1 评估堆栈(第 88 页)描述了评估堆栈,并列出了堆栈上可以表示的内容:

\n
\n

虽然 CLI 通常支持 \xc2\xa712.1 中描述的全套类型,但 CLI 以特殊的方式处理计算堆栈。虽然某些 JIT 编译器可能会更详细地跟踪堆栈上的类型,但 CLI 仅\n要求该值为以下之一:

\n
    \n
  • int64, 8 字节有符号整数
  • \n
  • int32, 4 字节有符号整数
  • \n
  • native int,4 或 8 字节的有符号整数,以对目标体系结构更方便的为准
  • \n
  • F、浮点值(float32float64或底层硬件支持的其他表示形式)
  • \n
  • &, 托管指针
  • \n
  • O, 对象引用
  • \n
  • *,一个 \xe2\x80\x9c 瞬态指针,\xe2\x80\x9d 只能在单个方法体内使用,它指向已知位于非托管内存中的值(有关更多信息,请参阅 CIL 指令集规范详细信息。* 类型是在 CLI 内部生成的;它们不是由用户创建的)。
  • \n
  • 用户定义的值类型
  • \n
\n
\n

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

\n

如果我们进一步深入研究,分区 I \xc2\xa712.1.3 浮点数据类型处理(第 79 页)中也提到了这一点:

\n
\n

浮点数(静态、数组元素和类字段)的存储位置具有固定大小。支持的存储大小为float32float64在其他地方(在计算堆栈上、作为参数、作为返回类型和作为局部变量)浮点数都使用内部浮点类型表示

\n
\n

对于拼图的最后一块,我们需要了解 的确切定义conv.r8,它在Partiion III \xc2\xa73.27 conv.<to type>- 数据转换(第 68 页)中定义:

\n
\n

conv.r8: 转换为float64, 推入F入堆栈。

\n
\n

最后,转换的细节F分区 III \xc2\xa71.5 表 8:转换操作(第 20 页)F中定义 :(释义)

\n
\n

如果输入(来自评估堆栈)是F并且转换为“所有浮点类型”:更改精度\xc2\xb3

\n

\xc2\xb3从计算堆栈上可用的当前精度转换为指令指定的精度。如果堆栈的精度高于输出大小,则使用 IEC 60559:1989 \xe2\x80\x9cround-to-nearest\xe2\x80\x9d 模式执行转换,以计算结果的低位。

\n
\n

因此,在这种情况下,您应该将其读conv.r8作“从未指定的浮点格式转换为double”,而不是“从未指定的浮点格式转换为doubledouble。(尽管在这种情况下,我们可以非常确定F评估堆栈上已经是double精度的,因为它来自double。)

\n
\n

总结来说:

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