显式基类型转换时可能冗余操作码

Probably redundantly opcode when explicit base type cast

我在默认 C# 编译器 VS 2017 RC Enterprise 中注意到了这种行为

当 double/float 被强制转换为自身已知类型 Conv.R8/Conv.R4 被发射。 但是,如果转换对象或非浮点类型,则不会发生任何事情。

下面的示例是在 release 模式下编译的。在 debug IL 是相似的。 示例 C# 代码:

    private double _Double;
    private float _Float;
    private int _Int;
    private object _Object;

    private int IntConvertToInt()
    {
        int x = (int)_Int; //
        return x;
    }

    private int IntAssignToInt()
    {
        int x = _Int;
        return x;
    }

    private float FloatConvertToFloat()
    {
        float x = (float)_Float; //Additional OpCode
        return x;
    }

    private float FloatAssignToFloat()
    {
        float x = _Float;
        return x;
    }
    private double DoubleConvertToDouble()
    {
        double x = (double)_Double; //Additional OpCode
        return x;
    }

    private double DoubleAssignToDouble()
    {
        double x = _Double;
        return x;
    }
    private Object ObjectConvertToObject()
    {
        Object x = (Object)_Object;
        return x;
    }

    private Object ObjectAssignToObject()
    {
        Object x = _Object;
        return x;
    }

对应Il:

        .method private hidebysig 
    instance int32 IntConvertToInt () cil managed 
{
    // Method begins at RVA 0x2052
    // Code size 7 (0x7)
    .maxstack 8

    IL_0000: ldarg.0
    IL_0001: ldfld int32 ConversionTest.Program::_Int
    IL_0006: ret
} // end of method Program::IntConvertToInt

.method private hidebysig 
    instance int32 IntAssignToInt () cil managed 
{
    // Method begins at RVA 0x2052
    // Code size 7 (0x7)
    .maxstack 8

    IL_0000: ldarg.0
    IL_0001: ldfld int32 ConversionTest.Program::_Int
    IL_0006: ret
} // end of method Program::IntAssignToInt

.method private hidebysig 
    instance float32 FloatConvertToFloat () cil managed 
{
    // Method begins at RVA 0x205a
    // Code size 8 (0x8)
    .maxstack 8

    IL_0000: ldarg.0
    IL_0001: ldfld float32 ConversionTest.Program::_Float
    IL_0006: conv.r4 //here
    IL_0007: ret
} // end of method Program::FloatConvertToFloat

.method private hidebysig 
    instance float32 FloatAssignToFloat () cil managed 
{
    // Method begins at RVA 0x2063
    // Code size 7 (0x7)
    .maxstack 8

    IL_0000: ldarg.0
    IL_0001: ldfld float32 ConversionTest.Program::_Float
    IL_0006: ret
} // end of method Program::FloatAssignToFloat

.method private hidebysig 
    instance float64 DoubleConvertToDouble () cil managed 
{
    // Method begins at RVA 0x206b
    // Code size 8 (0x8)
    .maxstack 8

    IL_0000: ldarg.0
    IL_0001: ldfld float64 ConversionTest.Program::_Double
    IL_0006: conv.r8 //here
    IL_0007: ret
} // end of method Program::DoubleConvertToDouble

.method private hidebysig 
    instance float64 DoubleAssignToDouble () cil managed 
{
    // Method begins at RVA 0x2074
    // Code size 7 (0x7)
    .maxstack 8

    IL_0000: ldarg.0
    IL_0001: ldfld float64 ConversionTest.Program::_Double
    IL_0006: ret
} // end of method Program::DoubleAssignToDouble

.method private hidebysig 
    instance object ObjectConvertToObject () cil managed 
{
    // Method begins at RVA 0x207c
    // Code size 7 (0x7)
    .maxstack 8

    IL_0000: ldarg.0
    IL_0001: ldfld object ConversionTest.Program::_Object
    IL_0006: ret
} // end of method Program::ObjectConvertToObject

.method private hidebysig 
    instance object ObjectAssignToObject () cil managed 
{
    // Method begins at RVA 0x207c
    // Code size 7 (0x7)
    .maxstack 8

    IL_0000: ldarg.0
    IL_0001: ldfld object ConversionTest.Program::_Object
    IL_0006: ret
}

为什么 Conv.R4/Conv.R8 会这样? 这个操作码在那里重要还是可以安全 removed/trimmed 出来?

link 解释了真正发生的事情。引用最重要的部分:

The CLI specification in section 12.1.3 dictates an exact precision for floating point numbers, float and double, when used in storage locations. However it allows for the precision to be exceeded when floating point numbers are used in other locations like the execution stack, arguments return values, etc … What precision is used is left to the runtime and underlying hardware. This extra precision can lead to subtle differences in floating point evaluations between different machines or runtimes.

This is where the extra conv.r4 and conv.r8 instructions come in. Typically they are used to coerce non-floating point values into floating point values. One of their side effects though is the resulting value will have the exact precision specified by the type. This means when applied to a floating point value on the evaluation stack it will truncate it to the specified precision.

所以,回答你的具体问题,不,你不能安全地删除这些操作码。

来自同一 link 的另一个有趣信息是,此行为不受 c# 编译器规范的保证,截至目前,它是一个实现细节。它可能不会改变,因为这是所有以前编译器的行为,不仅是 VS 2017.