Time for a bigger example!
using System.Runtime.CompilerServices;
interface Number {
static abstract int Eval();
}
class ConstDead : Number {
public static int Eval() {
return 0xDEAD;
}
}
class ConstBeef : Number {
public static int Eval() {
return 0xBEEF;
}
}
class Concat<A,B> : Number
where A: Number
where B: Number
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int Eval() {
return (A.Eval() << 16) + B.Eval();
}
}
class Example {
int Run() {
return Concat<ConstDead,ConstBeef>.Eval();
}
}
This combines a few ideas from the previous section: The three calls to Eval are monomorphically inlined and folded. If you throw this code in
SharpLab, you'll find that
Example.Run() JIT compiles to two instructions:
L0000: mov eax, 0xdeadbeef
L0005: ret
The [MethodImpl(MethodImplOptions.AggressiveInlining)] attribute is necessary to persuade .NET to inline the calls. Sadly, it isn't always sufficient. We need to encourage the JIT to inline and monomorphize our code. Fortunately, there's a way to do this: structs! Since structs are value types that use an unknown amount of stack space, the JIT isn't allowed to just re-use polymorphic code. Our example code becomes:
using System.Runtime.CompilerServices;
interface Number {
int Eval();
}
struct ConstDead : Number {
public int Eval() {
return 0xDEAD;
}
}
struct ConstBeef : Number {
public int Eval() {
return 0xBEEF;
}
}
struct Concat<A,B> : Number
where A: struct, Number
where B: struct, Number
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int Eval() {
return (default(A).Eval() << 16) + default(B).Eval();
}
}
class Example {
int Run() {
return default(Concat<ConstDead,ConstBeef>).Eval();
}
}
By some miracle, the JIT is just smart enough to optimize this the same way as the static code. Presumably it helps that the structs are empty, but I'm not sure whether it actually matters.