How to detect boxing in IL?

197 Views Asked by At

I am trying to figure out whether a boxing occurs when you access an integer as a property of dynamic type

Here is the C# code:

using System.Dynamic;

dynamic o = new ExpandoObject();
int i = 2;

o.Int = 1;

i = o.Int;

Console.WriteLine(o.Int);
Console.WriteLine(i);

Here is what I get from ILSpy:

.method private hidebysig static 
    void '<Main>$' (
        string[] args
    ) cil managed 
{
    // Method begins at RVA 0x206c
    // Header size: 12
    // Code size: 428 (0x1ac)
    .maxstack 11
    .entrypoint
    .locals init (
        [0] object o,
        [1] int32 i
    )

    // dynamic val = new ExpandoObject();
    IL_0000: newobj instance void [System.Linq.Expressions]System.Dynamic.ExpandoObject::.ctor()
    IL_0005: stloc.0
    // int num = 2;
    IL_0006: ldc.i4.2
    IL_0007: stloc.1
    // (no C# code)
    IL_0008: ldsfld class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Runtime]System.Func`4<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, int32, object>> Program/'<>o__0'::'<>p__0'
    IL_000d: brfalse.s IL_0011

    IL_000f: br.s IL_004a

    IL_0011: ldc.i4.0
    IL_0012: ldstr "Int"
    IL_0017: ldtoken Program
    IL_001c: call class [System.Runtime]System.Type [System.Runtime]System.Type::GetTypeFromHandle(valuetype [System.Runtime]System.RuntimeTypeHandle)
    IL_0021: ldc.i4.2
    IL_0022: newarr [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo
    IL_0027: dup
    IL_0028: ldc.i4.0
    IL_0029: ldc.i4.0
    IL_002a: ldnull
    IL_002b: call class [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo::Create(valuetype [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfoFlags, string)
    IL_0030: stelem.ref
    IL_0031: dup
    IL_0032: ldc.i4.1
    IL_0033: ldc.i4.3
    IL_0034: ldnull
    IL_0035: call class [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo::Create(valuetype [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfoFlags, string)
    IL_003a: stelem.ref
    IL_003b: call class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSiteBinder [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.Binder::SetMember(valuetype [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpBinderFlags, string, class [System.Runtime]System.Type, class [System.Runtime]System.Collections.Generic.IEnumerable`1<class [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo>)
    IL_0040: call class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<!0> class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Runtime]System.Func`4<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, int32, object>>::Create(class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSiteBinder)
    IL_0045: stsfld class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Runtime]System.Func`4<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, int32, object>> Program/'<>o__0'::'<>p__0'

    // val.Int = 1;
    IL_004a: ldsfld class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Runtime]System.Func`4<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, int32, object>> Program/'<>o__0'::'<>p__0'
    IL_004f: ldfld !0 class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Runtime]System.Func`4<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, int32, object>>::Target
    IL_0054: ldsfld class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Runtime]System.Func`4<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, int32, object>> Program/'<>o__0'::'<>p__0'
    IL_0059: ldloc.0
    IL_005a: ldc.i4.1
    IL_005b: callvirt instance !3 class [System.Runtime]System.Func`4<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, int32, object>::Invoke(!0, !1, !2)
    // (no C# code)
    IL_0060: pop
    IL_0061: ldsfld class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Runtime]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, int32>> Program/'<>o__0'::'<>p__2'
    IL_0066: brfalse.s IL_006a

    IL_0068: br.s IL_008e

    IL_006a: ldc.i4.0
    IL_006b: ldtoken [System.Runtime]System.Int32
    IL_0070: call class [System.Runtime]System.Type [System.Runtime]System.Type::GetTypeFromHandle(valuetype [System.Runtime]System.RuntimeTypeHandle)
    IL_0075: ldtoken Program
    IL_007a: call class [System.Runtime]System.Type [System.Runtime]System.Type::GetTypeFromHandle(valuetype [System.Runtime]System.RuntimeTypeHandle)
    IL_007f: call class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSiteBinder [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.Binder::Convert(valuetype [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpBinderFlags, class [System.Runtime]System.Type, class [System.Runtime]System.Type)
    IL_0084: call class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<!0> class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Runtime]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, int32>>::Create(class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSiteBinder)
    IL_0089: stsfld class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Runtime]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, int32>> Program/'<>o__0'::'<>p__2'

    // num = val.Int;
    IL_008e: ldsfld class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Runtime]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, int32>> Program/'<>o__0'::'<>p__2'
    IL_0093: ldfld !0 class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Runtime]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, int32>>::Target
    IL_0098: ldsfld class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Runtime]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, int32>> Program/'<>o__0'::'<>p__2'
    IL_009d: ldsfld class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Runtime]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, object>> Program/'<>o__0'::'<>p__1'
    IL_00a2: brfalse.s IL_00a6

    IL_00a4: br.s IL_00d5

    IL_00a6: ldc.i4.0
    IL_00a7: ldstr "Int"
    IL_00ac: ldtoken Program
    IL_00b1: call class [System.Runtime]System.Type [System.Runtime]System.Type::GetTypeFromHandle(valuetype [System.Runtime]System.RuntimeTypeHandle)
    IL_00b6: ldc.i4.1
    IL_00b7: newarr [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo
    IL_00bc: dup
    IL_00bd: ldc.i4.0
    IL_00be: ldc.i4.0
    IL_00bf: ldnull
    IL_00c0: call class [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo::Create(valuetype [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfoFlags, string)
    IL_00c5: stelem.ref
    IL_00c6: call class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSiteBinder [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.Binder::GetMember(valuetype [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpBinderFlags, string, class [System.Runtime]System.Type, class [System.Runtime]System.Collections.Generic.IEnumerable`1<class [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo>)
    IL_00cb: call class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<!0> class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Runtime]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, object>>::Create(class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSiteBinder)
    IL_00d0: stsfld class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Runtime]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, object>> Program/'<>o__0'::'<>p__1'

    IL_00d5: ldsfld class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Runtime]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, object>> Program/'<>o__0'::'<>p__1'
    IL_00da: ldfld !0 class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Runtime]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, object>>::Target
    IL_00df: ldsfld class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Runtime]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, object>> Program/'<>o__0'::'<>p__1'
    IL_00e4: ldloc.0
    IL_00e5: callvirt instance !2 class [System.Runtime]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, object>::Invoke(!0, !1)
    IL_00ea: callvirt instance !2 class [System.Runtime]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, int32>::Invoke(!0, !1)
    IL_00ef: stloc.1
    // (no C# code)
    IL_00f0: ldsfld class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Runtime]System.Action`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, class [System.Runtime]System.Type, object>> Program/'<>o__0'::'<>p__4'
    IL_00f5: brfalse.s IL_00f9

    IL_00f7: br.s IL_0138

    IL_00f9: ldc.i4 256
    IL_00fe: ldstr "WriteLine"
    IL_0103: ldnull
    IL_0104: ldtoken Program
    IL_0109: call class [System.Runtime]System.Type [System.Runtime]System.Type::GetTypeFromHandle(valuetype [System.Runtime]System.RuntimeTypeHandle)
    IL_010e: ldc.i4.2
    IL_010f: newarr [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo
    IL_0114: dup
    IL_0115: ldc.i4.0
    IL_0116: ldc.i4.s 33
    IL_0118: ldnull
    IL_0119: call class [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo::Create(valuetype [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfoFlags, string)
    IL_011e: stelem.ref
    IL_011f: dup
    IL_0120: ldc.i4.1
    IL_0121: ldc.i4.0
    IL_0122: ldnull
    IL_0123: call class [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo::Create(valuetype [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfoFlags, string)
    IL_0128: stelem.ref
    IL_0129: call class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSiteBinder [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.Binder::InvokeMember(valuetype [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpBinderFlags, string, class [System.Runtime]System.Collections.Generic.IEnumerable`1<class [System.Runtime]System.Type>, class [System.Runtime]System.Type, class [System.Runtime]System.Collections.Generic.IEnumerable`1<class [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo>)
    IL_012e: call class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<!0> class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Runtime]System.Action`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, class [System.Runtime]System.Type, object>>::Create(class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSiteBinder)
    IL_0133: stsfld class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Runtime]System.Action`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, class [System.Runtime]System.Type, object>> Program/'<>o__0'::'<>p__4'

    // Console.WriteLine(val.Int);
    IL_0138: ldsfld class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Runtime]System.Action`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, class [System.Runtime]System.Type, object>> Program/'<>o__0'::'<>p__4'
    IL_013d: ldfld !0 class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Runtime]System.Action`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, class [System.Runtime]System.Type, object>>::Target
    IL_0142: ldsfld class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Runtime]System.Action`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, class [System.Runtime]System.Type, object>> Program/'<>o__0'::'<>p__4'
    IL_0147: ldtoken [System.Console]System.Console
    IL_014c: call class [System.Runtime]System.Type [System.Runtime]System.Type::GetTypeFromHandle(valuetype [System.Runtime]System.RuntimeTypeHandle)
    IL_0151: ldsfld class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Runtime]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, object>> Program/'<>o__0'::'<>p__3'
    IL_0156: brfalse.s IL_015a

    IL_0158: br.s IL_0189

    IL_015a: ldc.i4.0
    IL_015b: ldstr "Int"
    IL_0160: ldtoken Program
    IL_0165: call class [System.Runtime]System.Type [System.Runtime]System.Type::GetTypeFromHandle(valuetype [System.Runtime]System.RuntimeTypeHandle)
    IL_016a: ldc.i4.1
    IL_016b: newarr [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo
    IL_0170: dup
    IL_0171: ldc.i4.0
    IL_0172: ldc.i4.0
    IL_0173: ldnull
    IL_0174: call class [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo::Create(valuetype [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfoFlags, string)
    IL_0179: stelem.ref
    IL_017a: call class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSiteBinder [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.Binder::GetMember(valuetype [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpBinderFlags, string, class [System.Runtime]System.Type, class [System.Runtime]System.Collections.Generic.IEnumerable`1<class [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo>)
    IL_017f: call class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<!0> class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Runtime]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, object>>::Create(class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSiteBinder)
    IL_0184: stsfld class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Runtime]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, object>> Program/'<>o__0'::'<>p__3'

    IL_0189: ldsfld class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Runtime]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, object>> Program/'<>o__0'::'<>p__3'
    IL_018e: ldfld !0 class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Runtime]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, object>>::Target
    IL_0193: ldsfld class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Runtime]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, object>> Program/'<>o__0'::'<>p__3'
    IL_0198: ldloc.0
    IL_0199: callvirt instance !2 class [System.Runtime]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, object>::Invoke(!0, !1)
    // (no C# code)
    IL_019e: callvirt instance void class [System.Runtime]System.Action`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, class [System.Runtime]System.Type, object>::Invoke(!0, !1, !2)
    // Console.WriteLine(num);
    IL_01a3: nop
    IL_01a4: ldloc.1
    IL_01a5: call void [System.Console]System.Console::WriteLine(int32)
    // }
    IL_01aa: nop
    IL_01ab: ret
} // end of method Program::'<Main>$'
2

There are 2 best solutions below

0
JL0PD On BEST ANSWER

The best way to know something is to measure, so I've wrote a benchmark. It will measure allocation to dynamic compared to explicit boxing.

using System.Dynamic;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

BenchmarkRunner.Run<MyBench>();

[MemoryDiagnoser]
public class MyBench
{
    private dynamic _myObj = new ExpandoObject();

    [Benchmark]
    [Arguments(1)]
    public void SetInt(int x)
    {
        _myObj.Int = x;
    }

    [Benchmark(Baseline = true)]
    [Arguments(1)]
    public object BoxInt(int x)
    {
        return x;
    }
}

Which gives following result:

BenchmarkDotNet=v0.13.5, OS=Windows 10 (10.0.19044.2728/21H2/November2021Update)
AMD Ryzen 5 3600X, 1 CPU, 12 logical and 6 physical cores
.NET SDK=7.0.100
  [Host]     : .NET 7.0.0 (7.0.22.51805), X64 RyuJIT AVX2
  DefaultJob : .NET 7.0.0 (7.0.22.51805), X64 RyuJIT AVX2
Method x Mean Error StdDev Ratio RatioSD Gen0 Allocated Alloc Ratio
SetInt 1 100.150 ns 0.2788 ns 0.2608 ns 35.81 0.15 0.0029 24 B 1.00
BoxInt 1 2.796 ns 0.0105 ns 0.0098 ns 1.00 0.00 0.0029 24 B 1.00

As you can see, both methods allocate same way - 24 bytes per method call. I bet int gets boxed somewhere inside this machinery

2
Guru Stron On

It seems that you can't, no boxing in this example happens "at compile time", it will happen at runtime due to compiler and binder magic because of dynamic usage. If you check the C# decompilation you will see that o.Int = 1 assignment (the only place where boxing should happen) is transformed by compiler into something like (@sharplab.io):

object arg = new ExpandoObject();
if (<>o__0.<>p__0 == null)
{
    Type typeFromHandle = typeof(Program);
    CSharpArgumentInfo[] array = new CSharpArgumentInfo[2];
    array[0] = CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null);
    array[1] = CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.UseCompileTimeType | CSharpArgumentInfoFlags.Constant, null);
        <>o__0.<>p__0 = CallSite<Func<CallSite, object, int, object>>.Create(Microsoft.CSharp.RuntimeBinder.Binder.SetMember(CSharpBinderFlags.None, "Int", typeFromHandle, array));
}
<>o__0.<>p__0.Target(<>o__0.<>p__0, arg, 1); // assignment 

[CompilerGenerated]
private static class <>o__0
{
    public static CallSite<Func<CallSite, object, int, object>> <>p__0;
}

So the generated func is accepting the integer parameter so compiler will not emit IL boxing instruction at build time.