Is there any reason for the C# compiler to emit a conv.r8 when casting from double -> double ?
This looks to be completely unnecessary (casting from int -> int, char -> char, etc) does not emit equivalent conversion instructions (as you can see in generated IL for the I2I() method).
class Foo
{
double D2D(double d) => (double) d;
int I2I(int i) => (int) i;
}
results in the IL of:
.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
The short version is that the intermediate representation of
double/floatin the CLI is intentionally unspecified. As such the compiler will always emit an explicit cast fromdoubletodouble(orfloattofloat) in case it would change the meaning of an expression.It doesn't change the meaning in this case, but the compiler doesn't know that. (The JIT does though and will optimize it away.)
If you want all the gnitty gritty background details...
The ECMA-335 references below specifically come from the version with Microsoft-Specific implementation notes, which can be downloaded from here. (Note that since we're talking about IL I will be speaking from the perspective of the .NET Runtime's virtual machine, not from any particular processor architecture.)
The justification for why Roslyn emits this seemingly unnecessary instruction can be found in
CodeGenerator.EmitIdentityConversion:(Emphasis and formatting mine.)
The important thing to note here is the "permitted to use higher precision math". To understand why this is we need to understand how the runtime represents different types at a low level. The virtual machine used by the .NET Runtime is stack-based, all intermediate values go onto what is called the evaluation stack. (Not to be confused with the processor's call stack, which may or may not be used for things on the evaluation stack at runtime.)
Partition I §12.3.2.1 The Evaluation Stack (pg 88) describes the evaluation stack, and lists what can be represented on the stack:
Of note is the only floating point type being the
Ftype, which you'll notice is intentionally vague and does not represent a specific precision. (This is done to provide flexibility for runtime implementations since they have to run on many different processors, which may or may not prefer a specific level of precision for floating point operations.)If we dig around a little further, this is also mentioned in Partition I §12.1.3 Handling of floating-point data types (pg 79):
For the final piece of the puzzle, we need to understand the exact definition of
conv.r8, which is defined in Partiion III §3.27conv.<to type>- data conversion (pg 68):and finally, the specifics of converting
FtoFare defined in Partition III §1.5 Table 8: Conversion Operations (pg 20): (Paraphrased)So in this context you should read
conv.r8as "Convert from unspecified floating-point format todouble" rather than "Convert fromdoubletodouble". (Although in this case, we can be pretty sure thatFon the evaluation stack is alreadydoubleprecision since it's from adoubleargument.)So in summary:
float64type, but only for storage purposes.Ftype is must be used instead.doubleis actually changing the precision of an expression.Ftofloat64. (However the JIT does, and in this case will optimize away the cast at runtime.)