提问人:Vagaus 提问时间:9/3/2021 最后编辑:Vagaus 更新时间:9/5/2021 访问量:159
为什么将 double 转换为 double 会发出 conv.r8 IL 指令
Why casting double to double emits conv.r8 IL instruction
问:
C# 编译器在从 进行转换时是否发出 conv.r8 ?double -> 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
答:
简短的版本是 CLI 中 / 的中间表示是故意未指定的。因此,编译器将始终发出 from to(或 to)的显式强制转换,以防它会改变表达式的含义。double
float
double
double
float
float
在这种情况下,它不会改变含义,但编译器不知道这一点。(JIT确实如此,并且会对其进行优化。
如果你想要所有坚韧不拔的背景细节......
下面的 ECMA-335 参考特别来自带有 Microsoft 特定实施说明的版本,可以从此处下载。(请注意,由于我们谈论的是 IL,因此我将从 .NET 运行时的虚拟机的角度进行讨论,而不是从任何特定的处理器体系结构的角度进行讨论。
可以在 CodeGenerator.EmitIdentityConversion
中找到 Roslyn 发出此看似不必要的指令的理由:
从 to 或 to on 的显式标识转换 非常量必须保留为转换。隐式标识转换可以是 优化走。为什么?因为具有与 不同的语义。 前者四舍五入到 64 位精度;后者允许使用更高的 如果注册了
D1
,则为精确数学。double
double
float
float
(double)d1 + d2
d1 + d2
(强调和格式我的。
这里需要注意的重要一点是“允许使用更高精度的数学”。要理解为什么会这样,我们需要了解运行时如何在低层次上表示不同的类型。.NET 运行时使用的虚拟机是基于堆栈的,所有中间值都进入所谓的评估堆栈。(不要与处理器的调用堆栈混淆,后者在运行时可能用于也可能不用于评估堆栈上的内容。
分区 I §12.3.2.1 评估堆栈(第 88 页)描述了评估堆栈,并列出了堆栈上可以表示的内容:
虽然 CLI 通常支持 §12.1 中描述的全套类型,但 CLI 处理评估堆栈 以一种特殊的方式。虽然某些 JIT 编译器可能会更详细地跟踪堆栈上的类型,但仅 CLI 要求值为以下值之一:
int64
,一个 8 字节有符号整数int32
,一个 4 字节有符号整数native int
,则为 4 个字节或 8 个字节的有符号整数,以目标体系结构更方便者为准F
,浮点值(float32
、float64
或底层硬件支持的其他表示形式)&
、托管指针O
、对象引用- *,一个“瞬态指针”,只能在单个方法的主体中使用,它指向已知位于非托管内存中的值(有关详细信息,请参阅 CIL 指令集规范。 * 类型在 CLI 内部生成;它们不是由用户创建的)。
- 用户定义的值类型
值得注意的是,唯一的浮点类型是类型,您会注意到它故意模糊,并且不表示特定的精度。(这样做是为了为运行时实现提供灵活性,因为它们必须在许多不同的处理器上运行,这些处理器可能更喜欢也可能不喜欢浮点运算的特定精度级别。F
如果我们再深入一点,分区 I §12.1.3 浮点数据类型的处理(第 79 页)中也提到了这一点:
浮点数(静态数、数组元素和类字段)的存储位置是固定大小的。支持的存储大小为 和 。在其他任何地方(在计算堆栈上,作为参数、作为返回类型和作为局部变量),浮点数都使用内部浮点类型表示。
float32
float64
对于拼图的最后一部分,我们需要了解 的确切定义,该定义在 Partiion III §3.27 conv.<to type>
- 数据转换(第 68 页)中定义:conv.r8
conv.r8
:转换为 ,推上堆栈。float64
F
最后,转换到的细节在分区 III §1.5 表 8:转换操作(第 20 页)中定义:(释义)F
F
如果输入(来自评估堆栈)是 并且 convert-to 是 “All float types”: 更改精度³
F
³从评估堆栈上可用的当前精度转换为 指令。如果堆栈的精度高于输出大小,则使用 IEC 60559:1989“舍入到最近”模式,用于计算结果的低阶位。
因此,在此上下文中,您应该阅读为“从不指定的浮点格式转换为”,而不是“从转换为”。(尽管在这种情况下,我们可以非常确定,由于它来自参数,因此在评估堆栈上已经是精度。conv.r8
double
double
double
F
double
double
所以总而言之:
- .NET 运行时具有一种类型,但仅用于存储目的。
float64
- 出于计算目的(和传递参数),必须改用精度未指定的类型。
F
- 这意味着有时“不必要”的显式强制转换实际上会更改表达式的精度。
double
- C# 编译器不知道这是否重要,因此它总是发出从 到 的转换。(但是,JIT 会这样做,在这种情况下,它将在运行时优化强制转换。
F
float64
评论
float80
float64