Runtime: DynamicMethod calls without inlining

Created on 3 Apr 2020  路  6Comments  路  Source: dotnet/runtime

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
area-VM-coreclr tenet-performance

Most helpful comment

I guess Regex

Regex uses it, but the resulting methods are only invoked via delegates. I wouldn't expect any inlining to result.

All 6 comments

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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

omariom picture omariom  路  3Comments

jkotas picture jkotas  路  3Comments

btecu picture btecu  路  3Comments

EgorBo picture EgorBo  路  3Comments

bencz picture bencz  路  3Comments