JIT seems to be less aggressive in inlining with IL generated for calling DynamicMethod from another DynamicMethod. Not sure if it is expected/desirable behaviour and whether it can be easily fixed?
Simple code:
public class C {
public static int MethodA(int x) => MethodB(x, 1000);
public static int MethodB(int x, int y) => x * x + y;
}
is translated to:
.method public hidebysig static int32 MethodA (int32 x) cil managed
{
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldc.i4 1000
IL_0006: call int32 C::MethodB(int32, int32)
IL_000b: ret
}
.method public hidebysig static int32 MethodB (int32 x, int32 y) cil managed
{
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldarg.0
IL_0002: mul
IL_0003: ldarg.1
IL_0004: add
IL_0005: ret
}
and then JIT produces nice result of MethodA:
C.MethodA(Int32)
L0000: mov eax, ecx
L0002: imul eax, ecx
L0005: add eax, 0x3e8
L000a: ret
while in case ILGenerator producing exactly the same IL:
private static DynamicMethod EmitMethodA(DynamicMethod secondMethod)
{
DynamicMethod code = new DynamicMethod(string.Empty, typeof(int), new[] { typeof(int) });
var ilGen = code.GetILGenerator();
ilGen.Emit(OpCodes.Ldarg_0);
ilGen.Emit(OpCodes.Ldc_I4, 1000);
ilGen.EmitCall(OpCodes.Call, secondMethod, null);
ilGen.Emit(OpCodes.Ret);
return code;
}
private static DynamicMethod EmitMethodB()
{
DynamicMethod code = new DynamicMethod(string.Empty, typeof(int), new [] {typeof(int), typeof(int)});
var ilGen = code.GetILGenerator();
ilGen.Emit(OpCodes.Ldarg_0);
ilGen.Emit(OpCodes.Dup);
ilGen.Emit(OpCodes.Mul);
ilGen.Emit(OpCodes.Ldarg_1);
ilGen.Emit(OpCodes.Add);
ilGen.Emit(OpCodes.Ret);
return code;
}
JIT produces non-inlined methods:
> !u 00007ff9`c50b0080
Normal JIT generated code
DynamicClass.(Int32)
Begin 00007FF9C50B0080, size 12
00007ff9`c50b0080 bae8030000 mov edx,3E8h
00007ff9`c50b0085 48b87052fbc4f97f0000 mov rax,7FF9C4FB5270h
00007ff9`c50b008f 48ffe0 jmp rax
> !u 00007ff9`c50b00f0
Normal JIT generated code
DynamicClass.(Int32, Int32)
Begin 00007FF9C50B00F0, size 8
00007ff9`c50b00f0 8bc1 mov eax,ecx
00007ff9`c50b00f2 0fafc1 imul eax,ecx
00007ff9`c50b00f5 03c2 add eax,edx
00007ff9`c50b00f7 c3 ret
Inlines into 06000000 DynamicClass:(int):int
[0 IL=0006 TR=000002 06000000] [FAILED: noinline per IL/cached result] DynamicClass:(int,int):int
Budget: initialTime=96, finalTime=96, initialBudget=960, currentBudget=960
Budget: initialSize=404, finalSize=404
Looks like all DynamicMethod come with noinline and Jit doesn't even try to inline them. I guess it's set here: https://github.com/dotnet/runtime/blob/3ab97fc14fa69a7cf03d1f0c5fb009ab9194d235/src/coreclr/src/vm/jitinterface.cpp#L6631-L6634
No idea what rationale is behind that
UPD Also, VM's canInline doesn't allow jit to inline it: "Inlinee is NoMetadata"
https://github.com/dotnet/runtime/blob/3ab97fc14fa69a7cf03d1f0c5fb009ab9194d235/src/coreclr/src/vm/jitinterface.cpp#L7873-L7878
No idea what rationale is behind that
The JIT/EE interface implementation does not handle the inlining of dynamic methods correctly in all situations. It is not a fundamental limitation; it is just something that would need to be fixed in number of places.
No idea what rationale is behind that
The JIT/EE interface implementation does not handle the inlining of dynamic methods correctly in all situations. It is not a fundamental limitation; it is just something that would need to be fixed in number of places.
Ah ok, here is the minimal set of changes needed to enable it back: https://github.com/EgorBo/runtime-1/commit/b5d1b0873709ce8e10a52aa3e50432008cf3bae5
And it works for the Konrad's case but yeah I guess like you said there are pitholes
Great insight, thank you! So there are pitholes, but it would be interesting to see how such change would influence existing standard libraries performance. No idea how to check this...
BTW, am I right that because of the same code @EgorBo has shown, the same behavior applies for AssemblyBuilder/TypeBuilder/MethodBuilder?
Great insight, thank you! So there are pitholes, but it would be interesting to see how such change would influence existing standard libraries performance. No idea how to check this...
BTW, am I right that because of the same code @EgorBo has shown, the same behavior applies for
AssemblyBuilder/TypeBuilder/MethodBuilder?
I can try the jit-diff in my branch to find out, I guess Regex and Linq.Expressions use it actively.
UPD ah, jit-diff won't show anything, the code must be executed first in order to trigger dynamic stuff. jit-diff only compiles it.
I guess Regex
Regex uses it, but the resulting methods are only invoked via delegates. I wouldn't expect any inlining to result.
Most helpful comment
Regex uses it, but the resulting methods are only invoked via delegates. I wouldn't expect any inlining to result.