Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does C# generate garbage when using a struct as a generic dictionary key?

I have a generic dictionary of a struct as key and a class reference as a value.

Dictionary<IntVector2, SpriteRenderer> tileRendererMap;

I retrieve a reference to the renderer via the coordinates and change it like so:

tileRendererMap[tileCoord].color = Color.cyan;

This generates 0.8KB of garbage each time I use it. Is this the default behaviour? I would think that dictionary lookup would be one of the most performant things to do. I need to find a way around this, since I'm working for mobile platform and it's a critical system.

Any ideas what I might be doing wrong or how to get allocation-free lookup?

Edit after more testing:

Using an int as key instead of my custom struct works as expected. No allocations, no problems and I don't think it's a weird thing to use an id as a key. However, for my game I want to store the renderer associated with a specific tile in a grid, which doesn't actually need to be any type of object, because I only care about the grid position. Hence, I thought an IntVector2 might be a practical identifier, but I could work around it, if I had to.

When using my struct I get allocations, which I measure with the Unity3D Profiler. It reports 0.8KB in Dictionary_get_item, specifically DefaultComparer.Equals(). Apparently it's a boxing/unboxing issue, but even when I implement my custom overrides, it still generates garbage, only a little less than before.

My basic struct implementation:

public struct IntVector2
{
    public int x, y;

    public override bool Equals(object obj)
    {
        if (obj == null || obj is IntVector2 == false)
            return false;

        var data = (IntVector2)obj;
        return x == data.x && y == data.y;
    }

    public override int GetHashCode()
    {
        return x.GetHashCode() ^ y.GetHashCode();
    }
}

Edit after accepted answer:

The allocation-free version of my struct when used in a generic dictionar.

public struct IntVector2 : IEquatable<IntVector2>
{
    public int x;
    public int y;

    public IntVector2(int x, int y)
    {
        this.x = x;
        this.y = y;
    }

    public bool Equals(IntVector2 other)
    {
        return x == other.x && y == other.y;
    }
}

The Dictionary class uses the IEquatable interface instead of the object override Equals, which is why my first implementation was boxing one of the values each time it checked for equality when searching for the key.

like image 354
Xarbrough Avatar asked Jun 13 '16 14:06

Xarbrough


People also ask

Why do we use %d in C?

In C programming language, %d and %i are format specifiers as where %d specifies the type of variable as decimal and %i specifies the type as integer. In usage terms, there is no difference in printf() function output while printing a number using %d or %i but using scanf the difference occurs.

Why semicolon is used in C?

Role of Semicolon in C: Semicolons are end statements in C. The Semicolon tells that the current statement has been terminated and other statements following are new statements. Usage of Semicolon in C will remove ambiguity and confusion while looking at the code.

Why do we write in C?

It was mainly developed as a system programming language to write an operating system. The main features of the C language include low-level memory access, a simple set of keywords, and a clean style, these features make C language suitable for system programmings like an operating system or compiler development.

Does C# exist?

As of July 2022, the most recent stable version of the language is C# 10.0, which was released in 2021 in . NET 6.0.


1 Answers

Since you only override Equals, and don't implement IEquatable<IntVector2>, the dictionary is forced to box one of the two instances whenever it compares two of them for equality, since it's passing an instance into an Equals method accepting object.

If you implement IEquatable<IntVector2> then the dictionary can (and will) use the version of Equals that accepts the parameter as an IntVector2, which won't require boxing.

like image 109
Servy Avatar answered Oct 12 '22 01:10

Servy