Probably redundantly opcode when explicit base type cast

88 views Asked by At

I noticed this behavior at default C# compiler, VS 2017 RC Enterprise

When double/float is casted to itself known type Conv.R8/Conv.R4 is emitted. However if object or non float point type is casted nothing happens.

Examples below has been compiled at release mode. At debug IL is similar. Sample C# Code:

    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;
    }

Coresponding 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
}

Why Conv.R4/Conv.R8 is behaving this way? Is this opcode there important or can it be safety removed/trimmed out?

1

There are 1 answers

1
InBetween On BEST ANSWER

This link explains what is really happening. To quote the most significant part:

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.

So, answering your specific question, no, you can not safely remove these opcodes.

Another interesting bit of information from that same link, is that this behaviour is not guaranteed by the c# compiler specification and it is, as of now, an implementation detail. Its probably not going to change as this is the behavior of all previous compilers, not only VS 2017.