【问题标题】:Why does C# generate garbage when using a struct as a generic dictionary key?为什么使用结构作为通用字典键时 C# 会产生垃圾?
【发布时间】:2016-06-13 14:51:35
【问题描述】:

我有一个结构体作为键和类引用作为值的通用字典。

Dictionary<IntVector2, SpriteRenderer> tileRendererMap;

我通过坐标检索对渲染器的引用并将其更改如下:

tileRendererMap[tileCoord].color = Color.cyan;

每次我使用它都会产生 0.8KB 的垃圾。这是默认行为吗?我认为字典查找将是最高效的事情之一。我需要想办法解决这个问题,因为我正在为移动平台工作,它是一个关键系统。

任何想法我可能做错了什么或如何获得免分配查找?

在更多测试后编辑:

使用 int 作为键而不是我的自定义结构按预期工作。没有分配,没有问题,我认为使用 id 作为键并不奇怪。但是,对于我的游戏,我想将与特定图块关联的渲染器存储在网格中,它实际上不需要是任何类型的对象,因为我只关心网格位置。因此,我认为 IntVector2 可能是一个实用的标识符,但如果必须,我可以解决它。

使用我的结构时,我会得到分配,我使用 Unity3D Profiler 进行测量。它在 Dictionary_get_item 中报告 0.8KB,特别是 DefaultComparer.Equals()。显然这是一个装箱/拆箱问题,但即使我实现了自定义覆盖,它仍然会产生垃圾,只是比以前少一点。

我的基本结构实现:

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();
    }
}

接受答案后编辑:

在通用字典中使用时我的结构的免分配版本。

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;
    }
}

Dictionary 类使用 IEquatable 接口而不是 object 覆盖 Equals,这就是为什么我的第一个每次在搜索键时检查是否相等时,实现都会对其中一个值进行装箱。

【问题讨论】:

  • 0.8kb 的垃圾应该会在需要时自动收集。
  • 您如何确定创建了 0.8 kb 的垃圾?你分析过那些垃圾包含的类型吗?
  • 它根本不会产生任何垃圾,您只是在 SpriteRenderer 对象上设置一个属性。 “颜色”不太可能是引用类型,甚至更不可能占用 800 个字节。你测量垃圾的方式有问题。
  • 我正在使用 Unity 3D Profiler 进行测量。它在 dictionary_get_item 中报告分配,特别是在 DefaultComparer Equals 方法中。我现在正在测试是否可以通过更改比较器来获得不同的结果。

标签: c# dictionary garbage-collection


【解决方案1】:

由于您只覆盖 Equals,而没有实现 IEquatable&lt;IntVector2&gt;,因此字典在比较其中两个实例是否相等时会强制将其中一个实例装箱,因为它会将实例传递给 Equals方法接受object

如果您实现IEquatable&lt;IntVector2&gt;,那么字典可以(并且将)使用Equals 的版本,该版本接受参数作为IntVector2,不需要装箱。

【讨论】:

  • 做到了。从探查器消息中我推断它正在调用常规覆盖 Equals,但使用接口它确实成为无垃圾调用。正是我所希望的。我仍然应该重新考虑字典键在这种情况下是否有意义。
【解决方案2】:

您的IntVector2struct 类型。不确定使用struct 作为字典键有什么意义?请记住struct 是值类型,因此将为每个键复制值,并且根据该结构的大小可能会产生内存占用。

您可能希望使用该结构的特定属性,例如。示例你的结构

public struct IntVector2
{
  string key;
}

然后你可以像使用那个属性一样

Dictionary<string, SpriteRenderer> tileRendererMap;

Intvector2 iv2 = new Intvector2();
iv2.key = "test";

tileRendererMap = new Dictionary<string, SpriteRenderer>();
tileRendererMap.Add(iv2.key, new SpriteRenderer{prop1 = value});

【讨论】:

  • 那么分配一个值给作为结构的字典键首先为键构造一个全新的结构,然后将新值分配给它?为什么在找到不可变结构后不将其单独保留并为其分配新的字典值?
  • @Quantic - 即使您在键周围使用方括号,您仍在调用方法并将结构作为参数传递,因此仍必须将其复制到堆栈中。
  • “不确定将结构用作字典键有什么意义?”...嗯,人们一直使用int 作为字典键,而int 是一个结构.
  • @RichardIrons 嗯,这是有道理的。我想我们需要知道探查器是如何工作的,因为将结构参数复制到堆栈不应显示为 GC 必须处理的“垃圾”,对吧? OP 是否只是看到堆栈内存使用量增加而他将其归因于“垃圾”?
  • Unity3D Profiler 测量“GC 分配”,它只为我的结构报告一些内容,而不是在使用类或原始类型(如 int)时报告。显然,我必须在我的结构实现中做错了什么。我也很确定,Profiler 只测量实际将收集的内容。我创建了一个干净的测试项目,以确保我没有看到任何其他内容。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-10-28
  • 2013-05-04
  • 2014-10-28
  • 2020-12-17
相关资源
最近更新 更多