Why does generic method with constraint of T: class result in boxing? [duplicate]
Asked Answered
B

4

12

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?

Barron answered 9/9, 2009 at 15:31 Comment(4)
The gist of the answer I have linked is that a boxing instruction on a reference type is effectively a nop. This allows the compiler to freely emit boxing instructions that can be removed by the JIT for closed constructed types that were created with a reference type as the generic type argument. In your case (since T is constrained as a reference type) neither of the two boxing instructions that were emitted would ever be run.Synecious
Those will be no-ops for reference types anyway, so it's not a big deal, but I have a suspicion. Are you compiling with /optimize+?Coffeehouse
Thanks Andrew. I did search around on generics and boxing but didn't find that question. I guess then, that the compiler doesn't bother implementing any special logic for this - since the box operation ends up doing nothing. Not sure, if this is the right way to do this, but if you want to post this as an answer, I'll mark it as accepted. Cheers!Barron
In answer to your question Pavel - don't think so - this is IL from a debug build, and the project settings has the 'Optimize Code' checkbox unchecked.Barron
B
2

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?).

Bracknell answered 17/9, 2009 at 15:12 Comment(0)
P
1

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.

Plier answered 9/9, 2009 at 16:5 Comment(1)
Lol. While this is indeed a proper solution (and, if I'm bragging, one which happens to now be trivially available to me, thanks to the strenuous efforts of an arduous and long-fought journey towards the elusive and mystical mixed C#/IL assembly), I doubt most anyone would be prepared to switch from C# to IL just to facilitate this bug fix.Hertha
D
0

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

Dolan answered 9/9, 2009 at 16:0 Comment(4)
If you do T : ActualClass, why bother with the generics?Petticoat
Because you can constrain T to higher levels... like iSomeInterface...Dolan
Chris, if T were an object, wouldn't it have already been boxed prior to pushing on the stack? Why then would any boxing operation need to be performed on it? I would expect the == operator to check reference equality if T were an object, so this also would not require un/boxing operations.Barron
@RobertHarvey Just to lay this 10-year-old dispute to rest, and also to confirm that this unfortunate C# behavior still prevails in 2019, the unwarranted boxing instructions a̲r̲e̲ i̲n̲d̲e̲e̲d̲ s̲t̲i̲l̲l e̲m̲i̲t̲t̲e̲d̲ even when constraining to some derived class where T : ActualClass, as opposed to what the OP specified where T : class, where the latter presumably implicates some "least-derived reference type" (and note--not System.Object, from which System.ValueType theoretically derives).Hertha
H
0

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

Hertha answered 10/2, 2019 at 6:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.