【问题标题】:Why casting double to double emits conv.r8 IL instruction为什么将 double 转换为 double 会发出 conv.r8 IL 指令
【发布时间】:2021-09-03 15:06:48
【问题描述】:

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

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

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

结果为:

.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.

【问题讨论】:

  • 我相信有一次它被用来强制 CPU 内部的 float80float64 但查看生成的 ASM 似乎并没有这样做
  • 有趣.. 我什至没有费心去研究 ASM :)
  • 我目前最好的猜测是老实说,它目前并未将其识别为可优化的。它认识到 int 不必强制转换并且可以安全地返回。如果你没有明确地投双倍,它不会发出一个 conv.r8 ... 可能值得在他们的 github 上询问

标签: c# compilation cil intermediate-language


【解决方案1】:

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

在这种情况下它不会改变含义,但编译器不知道这一点。 (尽管 JIT 会进行并将其优化掉。)


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

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

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

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

(强调和格式化我的。)

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

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

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

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

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

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

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

对于最后一块拼图,我们需要了解conv.r8 的确切定义,它在Partiion III §3.27 conv.&lt;to type&gt; - 数据转换(第 68 页)中定义

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

最后,将F 转换为F 的具体细节在第 III 部分 §1.5 表 8:转换操作(第 20 页)中定义:(释义)

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

³从评估堆栈上可用的当前精度转换为指定的精度 该指令。如果堆栈的精度高于输出大小,则使用执行转换 使用 IEC 60559:1989“取整”模式来计算结果的低位。

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


总之:

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

【讨论】:

  • 非常感谢您的详细解释:)。这让我很清楚,我的实现也必须发出:)
  • 没问题,很高兴我能帮上忙!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-04-01
  • 2016-10-10
  • 1970-01-01
  • 2019-07-08
相关资源
最近更新 更多