为什么将 double 转换为 double 会发出 conv.r8 IL 指令
Why casting double to double emits conv.r8 IL instruction
当从 double -> double
进行 casting 时,C# 编译器是否有任何理由发出 conv.r8?
这看起来完全没有必要(从 int -> int、char -> char 等进行转换)不会发出等效的转换指令(正如您在为 I2I()
方法生成的 IL 中看到的那样)。
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 中 double
/float
的中间表示有意未指定。因此,编译器将始终发出从 double
到 double
(或 float
到 float
)的显式转换,以防它会改变表达式的含义。
在这种情况下并没有改变意思,但是编译器不知道。 (虽然 JIT 会做,并且会优化它。)
如果你想要所有粗糙的背景细节...
下面的 ECMA-335 引用特别来自具有 Microsoft 特定实施说明的版本,can be downloaded from here。 (请注意,由于我们谈论的是 IL,所以我将从 .NET 运行时的虚拟机的角度而不是从任何特定的处理器体系结构的角度来谈论。)
可以在 CodeGenerator.EmitIdentityConversion
:
中找到 Roslyn 发出这个看似不必要的指令的理由
An explicit identity conversion from double
to double
or float
to float
on
non-constants must stay as a conversion. An implicit identity conversion can be
optimized away. Why? Because (double)d1 + d2
has different semantics than d1 + d2
.
The former rounds off to 64 bit precision; the latter is permitted to use higher
precision math if d1
is enregistered.
(强调和格式化我的。)
这里要注意的重要一点是“允许使用更高精度的数学”。要理解为什么会这样,我们需要了解 运行time 如何在低级别表示不同的类型。 .NET 运行时使用的虚拟机是基于堆栈的,所有中间值都进入所谓的评估堆栈。 (不要与处理器的调用堆栈混淆,它可能会或可能不会用于 运行 时间计算堆栈上的东西。)
分区 I §12.3.2.1 评估堆栈(第 88 页) 描述了评估堆栈,并列出了可以在堆栈上表示的内容:
While the CLI, in general, supports the full set of types described in §12.1, the CLI treats the evaluation stack
in a special way. While some JIT compilers might track the types on the stack in more detail, the CLI only
requires that values be one of:
int64
, an 8-byte signed integer
int32
, a 4-byte signed integer
native int
, a signed integer of either 4 or 8 bytes, whichever is more convenient for the target architecture
F
, a floating point value (float32
, float64
, or other representation supported by the underlying hardware)
&
, a managed pointer
O
, an object reference
- *, a “transient pointer,” which can be used only within the body of a single method, that points to a value known to be in unmanaged memory (see the CIL Instruction Set specification for more details. * types are generated internally within the CLI; they are not created by the user).
- A user-defined value type
值得注意的是,唯一的浮点类型是 F
类型,您会注意到它故意含糊不清,并不代表特定的精度。 (这样做是为了为 运行 时间实现提供灵活性,因为它们必须 运行 在许多不同的处理器上,这些处理器可能会或可能不会更喜欢浮点运算的特定精度级别。)
如果我们再深入一点,分区 I §12.1.3 浮点数据类型的处理(第 79 页):[=62= 中也提到了这一点]
Storage locations for floating-point numbers (statics, array elements, and fields of classes) are of fixed size. The supported storage sizes are float32
and float64
. Everywhere else (on the evaluation stack, as arguments, as return types, and as local variables) floating-point numbers are represented using an internal floating-point type.
对于最后一块拼图,我们需要了解conv.r8
的确切定义,定义在Partiion III §3.27 conv.<to type>
- 数据转换(pg 68):
conv.r8
: Convert to float64
, pushing F
on stack.
最后,将 F
转换为 F
的细节在 Partition III §1.5 Table 8: Conversion Operations (pg 20):(释义)
If input (from the evaluation stack) is F
and convert-to is "All float types": Change precision³
³Converts from the current precision available on the evaluation stack to the precision specified by
the instruction. If the stack has more precision than the output size the conversion is performed using
the IEC 60559:1989 “round-to-nearest” mode to compute the low order bit of the result.
所以在这种情况下,您应该将 conv.r8
理解为“从未指定的浮点格式转换为 double
”而不是“从 double
转换为 double
” . (虽然在这种情况下,我们可以非常确定计算堆栈上的 F
已经是 double
精度,因为它来自 double
参数。)
总而言之:
- .NET 运行时有一个
float64
类型,但仅用于存储目的。
- 出于评估目的(和传递参数),必须改用未指定精度的
F
类型。
- 这意味着有时“不必要的”显式转换为
double
实际上会改变表达式的精度。
- C# 编译器不知道它是否重要,所以它总是发出从
F
到 float64
的转换。 (但是 JIT 确实如此,在这种情况下,将在 运行 时间优化演员表。)
当从 double -> double
进行 casting 时,C# 编译器是否有任何理由发出 conv.r8?
这看起来完全没有必要(从 int -> int、char -> char 等进行转换)不会发出等效的转换指令(正如您在为 I2I()
方法生成的 IL 中看到的那样)。
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 中 double
/float
的中间表示有意未指定。因此,编译器将始终发出从 double
到 double
(或 float
到 float
)的显式转换,以防它会改变表达式的含义。
在这种情况下并没有改变意思,但是编译器不知道。 (虽然 JIT 会做,并且会优化它。)
如果你想要所有粗糙的背景细节...
下面的 ECMA-335 引用特别来自具有 Microsoft 特定实施说明的版本,can be downloaded from here。 (请注意,由于我们谈论的是 IL,所以我将从 .NET 运行时的虚拟机的角度而不是从任何特定的处理器体系结构的角度来谈论。)
可以在 CodeGenerator.EmitIdentityConversion
:
An explicit identity conversion from
double
todouble
orfloat
tofloat
on non-constants must stay as a conversion. An implicit identity conversion can be optimized away. Why? Because(double)d1 + d2
has different semantics thand1 + d2
. The former rounds off to 64 bit precision; the latter is permitted to use higher precision math ifd1
is enregistered.
(强调和格式化我的。)
这里要注意的重要一点是“允许使用更高精度的数学”。要理解为什么会这样,我们需要了解 运行time 如何在低级别表示不同的类型。 .NET 运行时使用的虚拟机是基于堆栈的,所有中间值都进入所谓的评估堆栈。 (不要与处理器的调用堆栈混淆,它可能会或可能不会用于 运行 时间计算堆栈上的东西。)
分区 I §12.3.2.1 评估堆栈(第 88 页) 描述了评估堆栈,并列出了可以在堆栈上表示的内容:
While the CLI, in general, supports the full set of types described in §12.1, the CLI treats the evaluation stack in a special way. While some JIT compilers might track the types on the stack in more detail, the CLI only requires that values be one of:
int64
, an 8-byte signed integerint32
, a 4-byte signed integernative int
, a signed integer of either 4 or 8 bytes, whichever is more convenient for the target architectureF
, a floating point value (float32
,float64
, or other representation supported by the underlying hardware)&
, a managed pointerO
, an object reference- *, a “transient pointer,” which can be used only within the body of a single method, that points to a value known to be in unmanaged memory (see the CIL Instruction Set specification for more details. * types are generated internally within the CLI; they are not created by the user).
- A user-defined value type
值得注意的是,唯一的浮点类型是 F
类型,您会注意到它故意含糊不清,并不代表特定的精度。 (这样做是为了为 运行 时间实现提供灵活性,因为它们必须 运行 在许多不同的处理器上,这些处理器可能会或可能不会更喜欢浮点运算的特定精度级别。)
如果我们再深入一点,分区 I §12.1.3 浮点数据类型的处理(第 79 页):[=62= 中也提到了这一点]
Storage locations for floating-point numbers (statics, array elements, and fields of classes) are of fixed size. The supported storage sizes are
float32
andfloat64
. Everywhere else (on the evaluation stack, as arguments, as return types, and as local variables) floating-point numbers are represented using an internal floating-point type.
对于最后一块拼图,我们需要了解conv.r8
的确切定义,定义在Partiion III §3.27 conv.<to type>
- 数据转换(pg 68):
conv.r8
: Convert tofloat64
, pushingF
on stack.
最后,将 F
转换为 F
的细节在 Partition III §1.5 Table 8: Conversion Operations (pg 20):(释义)
If input (from the evaluation stack) is
F
and convert-to is "All float types": Change precision³³Converts from the current precision available on the evaluation stack to the precision specified by the instruction. If the stack has more precision than the output size the conversion is performed using the IEC 60559:1989 “round-to-nearest” mode to compute the low order bit of the result.
所以在这种情况下,您应该将 conv.r8
理解为“从未指定的浮点格式转换为 double
”而不是“从 double
转换为 double
” . (虽然在这种情况下,我们可以非常确定计算堆栈上的 F
已经是 double
精度,因为它来自 double
参数。)
总而言之:
- .NET 运行时有一个
float64
类型,但仅用于存储目的。 - 出于评估目的(和传递参数),必须改用未指定精度的
F
类型。 - 这意味着有时“不必要的”显式转换为
double
实际上会改变表达式的精度。 - C# 编译器不知道它是否重要,所以它总是发出从
F
到float64
的转换。 (但是 JIT 确实如此,在这种情况下,将在 运行 时间优化演员表。)