Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

.Net Framework 4.0 - Opcodes.Box present in Dictionary with int key

I'm trying to investigate whether dictionaries with enum keys still generate garbage in newer versions of .Net (say >= 4)

See Shawn Hargreaves blog post here for details on why I'm even fretting about this... (http://blogs.msdn.com/b/shawnhar/archive/2007/07/02/twin-paths-to-garbage-collector-nirvana.aspx) Very specific I know but garbage on the xbox is / can be a very really problem.

I created a little .Net v4 console application comparing the IL generated for Dictionary and Dicationary and noticed a 'box' opcode in both sets of code which really confused me.

.method private hidebysig 
    instance int32 FindEntry (
        !TKey key
    ) cil managed 
{
    // Method begins at RVA 0x61030
    // Code size 138 (0x8a)
    .maxstack 3
    .locals init (
        [0] int32,
        [1] int32
    )

    IL_0000: ldarg.1
    IL_0001: box !TKey   <----Hmmmm!
    IL_0006: brtrue.s IL_000e

    IL_0008: ldc.i4.5
    IL_0009: call void System.ThrowHelper::ThrowArgumentNullException(valuetype System.ExceptionArgument)

    IL_000e: ldarg.0
    IL_000f: ldfld int32[] class System.Collections.Generic.Dictionary`2<!TKey, !TValue>::buckets
    IL_0014: brfalse.s IL_0088

https://msdn.microsoft.com/en-us/library/system.reflection.emit.opcodes.box%28v=vs.110%29.aspx

Convert a value type (of the type specified in valTypeToken) to a true object reference.

Is the box here not a heap allocation? If not, then how can I tell when there's heap allocations that might cause the Xbox to struggle?(from looking at the IL) Does it depend on some other context? Would a memory profiler (CLR Profiler for example) be the only way to tell for sure?

like image 692
Paul Cunningham Avatar asked May 08 '15 07:05

Paul Cunningham


2 Answers

Yes it is a box, but no it shouldn't matter in this case - at least, not for regular .NET; this is a != null check; the JIT knows how to recognise these for value-types, and can remove that check from the machine code.

Allegedly.

To tell for sure, you'd need to look at the post-JIT machine code, not the IL.

It will also matter which JIT you are using, which makes it harder.

Worst case: you could use the CoreCLR code to roll your own value-type dictionary.

like image 119
Marc Gravell Avatar answered Nov 10 '22 11:11

Marc Gravell


I will say that problem was solved. I took the code from http://beardseye.blogspot.it/2007/08/nuts-enum-conundrum.html

enum TestEnum
{
    e10,
    e9,
    e8,
    e7,
    e6,
    e5,
    e4,
    e3,
    e2,
    e1
}

class Program
{
    static void Main(string[] args)
    {
        Dictionary<TestEnum, int> dict = new Dictionary<TestEnum, int>();

        for (int l = 0; l < 100000; l++)
        {
            TestEnum x = (TestEnum)(l % 10);
            dict[x] = 100000 - (int)x;
        }

        for (TestEnum x = TestEnum.e10; x <= TestEnum.e1; x++)
        {
            Console.WriteLine(dict[x]);
        }
    }
}

And I've run it with the profiler of Visual Studio 2013. There are no allocations of the TestEnum "object". Note that if I change the code to

dict[x] = 100000 - x.GetHashCode();

then I get many allocations of TestEnum.

Other test:

public class TestEnumComparer : IEqualityComparer<TestEnum>
{

    public bool Equals(TestEnum x, TestEnum y)
    {
        return x == y;
    }

    public int GetHashCode(TestEnum obj)
    {
        return obj.GetHashCode();
    }
}

and

Dictionary<TestEnum, int> dict = new Dictionary<TestEnum, int>(new TestEnumComparer());

Still I get many allocations of TestEnum.

So I'll say that EqualityComparer<T>.Default does something to not box enums.

If you look in the EqualityComparer source, in the CreateComparer() method:

// If T is an int-based Enum, return an EnumEqualityComparer<T>
// See the METHOD__JIT_HELPERS__UNSAFE_ENUM_CAST and METHOD__JIT_HELPERS__UNSAFE_ENUM_CAST_LONG cases in getILIntrinsicImplementation
if (t.IsEnum && Enum.GetUnderlyingType(t) == typeof(int))
{
    return (EqualityComparer<T>)RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType)typeof(EnumEqualityComparer<int>), t);
}

there is some special code to compare int enum and only them.

And I can confirm that if I change the enum to

enum TestEnum : long

then the allocations caused by the boxing appear! :-)

So, in the end

Dictionary<enum_that_is_an_int, Foo>

is safe to use,

every other type of enum isn't! :-)

Note that this is true for .NET >= 4.0 . I've looked at the CreateComparer() of mscorlib 2.0.0.0 and there isn't a check like this, so on .NET 2.0-3.5 the Dictionary<enum_that_is_an_int, Foo> isn't safe to use.

like image 37
xanatos Avatar answered Nov 10 '22 11:11

xanatos