Question

This question already has an answer here:

Why a generic method which constrains T to class would have boxing instructions in the generates MSIL code?

I was quite surprised by this since surely since T is being constrained to a reference type the generated code should not need to perform any boxing.

Here is the c# code:

protected void SetRefProperty<T>(ref T propertyBackingField, T newValue) where T : class
{
    bool isDifferent = false;

    // for reference types, we use a simple reference equality check to determine
    // whether the values are 'equal'.  We do not use an equality comparer as these are often
    // unreliable indicators of equality, AND because value equivalence does NOT indicate
    // that we should share a reference type since it may be a mutable.

    if (propertyBackingField != newValue)
    {
        isDifferent = true;
    }
}

Here is the generated IL:

.method family hidebysig instance void SetRefProperty<class T>(!!T& propertyBackingField, !!T newValue) cil managed
{
    .maxstack 2
    .locals init (
        [0] bool isDifferent,
        [1] bool CS$4$0000)
    L_0000: nop 
    L_0001: ldc.i4.0 
    L_0002: stloc.0 
    L_0003: ldarg.1 
    L_0004: ldobj !!T
    L_0009: box !!T
    L_000e: ldarg.2 
    L_000f: box !!T
    L_0014: ceq 
    L_0016: stloc.1 
    L_0017: ldloc.1 
    L_0018: brtrue.s L_001e
    L_001a: nop 
    L_001b: ldc.i4.1 
    L_001c: stloc.0 
    L_001d: nop 
    L_001e: ret 
}

Notice the box !!T instructions.

Why this is being generated?

How to avoid this?

Was it helpful?

Solution

You don't have to worry about any performance-degradations from the box instruction because if its argument is a reference type, the box instruction does nothing. Though it's still strange that the box instruction has even been created (maybe lazyiness/easier design at code generation?).

OTHER TIPS

I'm not sure why any boxing is ocurring. One possible way to avoid the boxing is to not use it. Just recompile without the boxing. Ex:

.assembly recomp_srp
{
    .ver 1:0:0:0
}

.class public auto ansi FixedPBF
{

.method public instance void .ctor() cil managed
{

}

.method hidebysig public instance void SetRefProperty<class T>(!!T& propertyBackingField, !!T newValue) cil managed
{
    .maxstack 2    
        .locals init ( bool isDifferent, bool CS$4$0000)

        ldc.i4.0
        stloc.0
        ldarg.1
        ldobj !!T
        ldarg.2
        ceq
        stloc.1
        ldloc.1
        brtrue.s L_0001
        ldc.i4.1
        stloc.0
        L_0001: ret

}

}

...if you save to a file recomp_srp.msil you can simply recompile as such:

ildasm /dll recomp_srp.msil

And it runs OK without the boxing on my end:

        FixedPBF TestFixedPBF = new FixedPBF();

        TestFixedPBF.SetRefProperty<string>(ref TestField, "test2");

...of course, I changed it from protected to public, you would need to make the change back again and provide the rest of your implementation.

I believe this is intended by design. You're not constraining T to a specific class so it's most likely down casting it to object. Hence why you see the IL include boxing.

I would try this code with where T : ActualClass

Following up on a couple points. First of all, this bug occurs for both methods in a generic class with constraint where T : class and also generic methods with that same constraint (in a generic or non-generic class). It does not occur for an (otherwise identical) non-generic method which uses Object instead of T:

// static T XchgNullCur<T>(ref T addr, T value) where T : class =>
//              Interlocked.CompareExchange(ref addr, val, null) ?? value;
    .locals init (!T tmp)
    ldarg addr
    ldarg val
    ldloca tmp
    initobj !T
    ldloc tmp
    call !!0 Interlocked::CompareExchange<!T>(!!0&, !!0, !!0)
    dup 
    box !T
    brtrue L_001a
    pop 
    ldarg val
L_001a:
    ret 


// static Object XchgNullCur(ref Object addr, Object val) =>
//                   Interlocked.CompareExchange(ref addr, val, null) ?? value;
    ldarg addr
    ldarg val
    ldnull
    call object Interlocked::CompareExchange(object&, object, object)
    dup
    brtrue L_000d
    pop
    ldarg val
L_000d:
    ret

Notice some additional problems with the first example. Instead of simply ldnull we have an extraneous initobj call pointlessly targeting an excess local variable tmp.

The good news however, hinted-at here, is that none of this matters. Despite the differences in the IL code generated for the two examples above, the x64 JIT generates nearly identical code for them. The following result is for .NET Framework 4.7.2 release mode with optimization "not-suppressed".

enter image description here

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top