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

Why casting double to double emits conv.r8 IL instruction

提问人:Vagaus 提问时间:9/3/2021 最后编辑:Vagaus 更新时间:9/5/2021 访问量:159

问:

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

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

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

您也可以使用上面的代码

C# 编译 CIL 中间语言

评论

1赞 Charlieface 9/3/2021
我相信它曾经被用来强制 CPU 内部进入 a,但看看生成的 ASM,它似乎并没有这样做float80float64
0赞 Vagaus 9/3/2021
有趣。。我什至懒得研究 ASM :)
1赞 Vulpex 9/3/2021
老实说,我目前最好的猜测只是它目前没有将其确定为可优化的事实。它识别出 int 不必被强制转换,并且可以安全地返回。如果您不显式转换双精度,则不会发出 conv.r8...可能值得在他们的 github 上问

答:

7赞 Pathogen David 9/5/2021 #1

简短的版本是 CLI 中 / 的中间表示是故意未指定的。因此,编译器将始终发出 from to(或 to)的显式强制转换,以防它会改变表达式的含义。doublefloatdoubledoublefloatfloat

在这种情况下,它不会改变含义,但编译器不知道这一点。(JIT确实如此,并且会对其进行优化。


如果你想要所有坚韧不拔的背景细节......

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

可以在 CodeGenerator.EmitIdentityConversion 中找到 Roslyn 发出此看似不必要的指令的理由:

从 to 或 to on 的显式标识转换 非常量必须保留为转换。隐式标识转换可以是 优化走。为什么?因为具有与 不同的语义。 前者四舍五入到 64 位精度;后者允许使用更高的 如果注册了 D1,则为精确数学。doubledoublefloatfloat(double)d1 + d2d1 + d2

(强调和格式我的。

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

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

虽然 CLI 通常支持 §12.1 中描述的全套类型,但 CLI 处理评估堆栈 以一种特殊的方式。虽然某些 JIT 编译器可能会更详细地跟踪堆栈上的类型,但仅 CLI 要求值为以下值之一:

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

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

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

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

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

conv.r8:转换为 ,推上堆栈。float64F

最后,转换到的细节在分区 III §1.5 表 8:转换操作(第 20 页)中定义:(释义)FF

如果输入(来自评估堆栈)是 并且 convert-to 是 “All float types”: 更改精度³F

³从评估堆栈上可用的当前精度转换为 指令。如果堆栈的精度高于输出大小,则使用 IEC 60559:1989“舍入到最近”模式,用于计算结果的低阶位。

因此,在此上下文中,您应该阅读为“从不指定的浮点格式转换为”,而不是“从转换为”。(尽管在这种情况下,我们可以非常确定,由于它来自参数,因此在评估堆栈上已经是精度。conv.r8doubledoubledoubleFdoubledouble


所以总而言之:

  • .NET 运行时具有一种类型,但仅用于存储目的。float64
  • 出于计算目的(和传递参数),必须改用精度未指定的类型。F
  • 这意味着有时“不必要”的显式强制转换实际上会更改表达式的精度。double
  • C# 编译器不知道这是否重要,因此它总是发出从 到 的转换。(但是,JIT 会这样做,在这种情况下,它将在运行时优化强制转换。Ffloat64

评论

1赞 Vagaus 9/5/2021
非常感谢您:)的详细解释。这让我清楚地知道,我的实现也必须发出这种:)
0赞 Pathogen David 9/6/2021
没问题,很高兴我能帮上忙!