Vag*_*aus 4 c# cil compilation intermediate-language
C# 编译器在从进行转换时是否有任何理由发出conv.r8?double -> 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)
简而言之,CLI 中double
/的中间表示是故意未指定的。float
因此,编译器总是会发出从double
to double
(或float
to float
)的显式转换,以防它改变表达式的含义。
在这种情况下它不会改变含义,但编译器不知道这一点。(JIT 确实会优化它。)
\n如果你想要所有的细节背景......
\n下面的 ECMA-335 参考资料具体来自带有 Microsoft 特定实施说明的版本,可以从此处下载。(请注意,由于我们正在讨论 IL,因此我将从 .NET 运行时虚拟机的角度进行讨论,而不是从任何特定的处理器架构进行讨论。)
\nRoslyn 发出这个看似不必要的指令的理由可以在以下位置找到CodeGenerator.EmitIdentityConversion
:
\n\n从到或到\n非常量的显式标识转换必须保留为转换。可以优化隐式身份转换。\n 为什么?因为与 具有不同的语义。\n前者四舍五入为 64 位精度;后者可以使用更高\n精度的数学,如果
\ndouble
double
float
float
(double)d1 + d2
d1 + d2
d1
。
(强调并格式化我的。)
\n这里要注意的重要一点是“允许使用更高精度的数学”。要理解为什么会这样,我们需要理解运行时如何在低级别上表示不同的类型。.NET 运行时使用的虚拟机是基于堆栈的,所有中间值都进入所谓的评估堆栈。(不要与处理器的调用堆栈混淆,它可能会也可能不会在运行时用于评估堆栈上的内容。)
\n分区 I \xc2\xa712.3.2.1 评估堆栈(第 88 页)描述了评估堆栈,并列出了堆栈上可以表示的内容:
\n\n\n虽然 CLI 通常支持 \xc2\xa712.1 中描述的全套类型,但 CLI 以特殊的方式处理计算堆栈。虽然某些 JIT 编译器可能会更详细地跟踪堆栈上的类型,但 CLI 仅\n要求该值为以下之一:
\n\n
\n- \n
int64
, 8 字节有符号整数- \n
int32
, 4 字节有符号整数- \n
native int
,4 或 8 字节的有符号整数,以对目标体系结构更方便的为准- \n
F
、浮点值(float32
、float64
或底层硬件支持的其他表示形式)- \n
&
, 托管指针- \n
O
, 对象引用- *,一个 \xe2\x80\x9c 瞬态指针,\xe2\x80\x9d 只能在单个方法体内使用,它指向已知位于非托管内存中的值(有关更多信息,请参阅 CIL 指令集规范详细信息。* 类型是在 CLI 内部生成的;它们不是由用户创建的)。
\n- 用户定义的值类型
\n
值得注意的是唯一的浮点类型是类型F
,您会注意到它是故意模糊的并且不代表特定的精度。(这样做是为了为运行时实现提供灵活性,因为它们必须在许多不同的处理器上运行,这些处理器可能更喜欢也可能不喜欢浮点运算的特定精度级别。)
如果我们进一步深入研究,分区 I \xc2\xa712.1.3 浮点数据类型处理(第 79 页)中也提到了这一点:
\n\n\n浮点数(静态、数组元素和类字段)的存储位置具有固定大小。支持的存储大小为
\nfloat32
和float64
。在其他地方(在计算堆栈上、作为参数、作为返回类型和作为局部变量)浮点数都使用内部浮点类型表示。
对于拼图的最后一块,我们需要了解 的确切定义conv.r8
,它在Partiion III \xc2\xa73.27 conv.<to type>
- 数据转换(第 68 页)中定义:
\n\n\n
conv.r8
: 转换为float64
, 推入F
入堆栈。
最后,转换的细节F
在分区 III \xc2\xa71.5 表 8:转换操作(第 20 页)F
中定义 :(释义)
\n\n如果输入(来自评估堆栈)是
\nF
并且转换为“所有浮点类型”:更改精度\xc2\xb3\xc2\xb3从计算堆栈上可用的当前精度转换为指令指定的精度。如果堆栈的精度高于输出大小,则使用 IEC 60559:1989 \xe2\x80\x9cround-to-nearest\xe2\x80\x9d 模式执行转换,以计算结果的低位。
\n
因此,在这种情况下,您应该将其读conv.r8
作“从未指定的浮点格式转换为double
”,而不是“从未指定的浮点格式转换为double
” double
。(尽管在这种情况下,我们可以非常确定F
评估堆栈上已经是double
精度的,因为它来自double
。)
总结来说:
\nfloat64
类型,但仅用于存储目的。F
出于评估目的(并传递参数),必须使用未指定精度的类型。double
实际上会改变表达式的精度。F
到 的转换float64
。(然而 JIT 确实如此,在这种情况下,它将在运行时优化掉转换。)