在C#中,是将一个大对象存储在内存中还是仅存储指向它的指针?

3
struct Tile
{
    GameObject go;
    ... //more fields

    public Tile(GameObject g, Object s)
    {
        go = g;
        ...
    }
}
Tile[,] tilemap = new Tile[1000,1000]

Tile结构包含一个GameObject(Unity Engine中已经存在于场景/内存中的游戏对象)和其他字段。考虑到我想要存储一百万个Tile,它可能占用大量内存。

我对C#中的指针没有太多经验。

只将指向GameObject的指针存储在结构体中是否明智?

struct Tile
{
    GameObject* go;
    ...
}

如果你正在设计大型类,使用 class 而不是 struct,并使用 new 创建类的对象,这样对象就会在大堆上创建,而不是小栈上。 - Tobias Knauss
3个回答

3

在C#中,引用实际上已经是一个指针(在底层)。它不是某个对象的副本。在你的第一个代码片段中,“go”实际上是一个指向实际对象的指针。你担心的事情并不真实。

ref(如“go”)与不安全指针之间的区别之一是,ref可以移动,垃圾回收器可以且将会(当它感觉合适时)移动对象(并自动更改ref)。

所以这里没有问题,只是一种错误的看法。


也许不是完全没有意义。@Hugh正确指出引用是一个指针,而@Graeme给出的答案错误地暗示复制结构体将导致GameObject的新副本。然而,我会质疑使用struct来声明Tile而不是声明为class的决定。正如Eric Lippert在https://dev59.com/G2DVa4cB1Zd3GeqPeIMQ#9315211中指出的那样,结构体的真正用例很少,因此可能会有性能损失。 - Daniel Hume
我已经根据@DanielHume的指出更新了我的答案,以便更加清晰。我并没有暗示会制作GameObject的副本,而是结构元素的副本,只是表述不太好。 - Graeme

2
您不能将类作为指针使用。也许在最新的C#中可能发生了改变,但我不确定。那根本行不通。任何引用类型都不能与指针一起使用。
像int、float和bool等简单类型可以用作指针。
即使您在Tile结构中声明GameObject go变量,也不行。
不建议在结构体内部声明引用类型,特别是在游戏应用程序中,因为这会使垃圾回收器不必要地扫描结构中要销毁的对象,从而降低速度。当您有数百万个带有引用对象的struct时,情况就更糟了。
我的建议是使用整数代替GameObject *。该整数应使用GameObject的instanceID进行初始化,而不是使用GameObject引用或指针。
然后,您可以使用Dictionary将instanceID(int)映射到GameObject。当您想要访问GameObject时,请提供存储在Tile结构中的intanceID。这将加快所有操作。
例如:
struct Tile
{
    int goID;

    public Tile(int goID)
    {
        this.goID = goID;
    }
}

使用方法:

Dictionary<int, GameObject> idToObj = new Dictionary<int, GameObject>();

Tile[] tiles = new Tile[1000];
GameObject obj = new GameObject("obj");

//Create new Instance with Instance ID
tiles[0] = new Tile(obj.GetInstanceID());

//Add to Dictionary
idToObj.Add(obj.GetInstanceID(), obj);

最后,值得测试一下看哪个更快。先使用在 struct 中声明的 GameObject 进行测试,然后再使用上面介绍的使用 intDictionary 的方法进行测试。


@AlexeiLevenkov 我知道有一个字典开销。你只需要在初始化期间将对象存储到字典中一次。只有当游戏开始时才会发生这种开销。这个开销就消失了。现在你拥有的是一个占用更多空间但比在结构体中存储数百万个类引用更快、更好的字典。当GC运行时,它必须扫描数百万个结构体以找到其中的类引用。也许我错了...从Tile结构体类中,你可以使用goID作为键来访问GameObject。 - Programmer
抱歉 @程序员,我的回复是针对你的。我对内部机制不是100%确定,所以这更多是我假设/提出的问题。 - Graeme
1
@Graeme 我所知道的所有 .Net 实现都没有使用传统的引用计数来进行垃圾回收(参见 https://en.wikipedia.org/wiki/Garbage_collection_(computer_science)#Strategies)。它们使用某种形式的“标记和清除”- https://en.wikipedia.org/wiki/Tracing_garbage_collection#Na.C3.AFve_mark-and-sweep (MSDN - https://msdn.microsoft.com/en-us/library/ee787088.aspx,Unity3d - https://docs.unity3d.com/Manual/UnderstandingAutomaticMemoryManagement.html)。因此,根本没有为引用计数分配空间。 - Alexei Levenkov
@程序员 我明白你的意思 - 确实从 struct 中删除所有引用将使GC扫描该数组更快。我建议将有关结构和GC的注释内联到帖子中(因为其他人可能会像我一样将其视为“内存使用”)。此外,这种更改需要仔细测量 - 您正在交换潜在的GC效率与每个帧上增加的查找成本(dictionary[id]不是免费的)。我的猜测结果将严重依赖于10^6个图块中有多少对象 - 更多的图块具有对对象的引用,这种优化就越不可能有用。 - Alexei Levenkov
1
@AlexeiLevenkov 修改了我的答案并要求 OP 进行测试以查看哪个更快。如果有问题,您可以编辑答案。 - Programmer
显示剩余7条评论

1
据我了解,GameObject已经是引用类型,因此它是对对象而不是实际对象本身的引用。
因此,只需简单地存储。
struct Tile {
  GameObject go;
  ...
}

如果您只是在内部使用结构体,则传递该结构体的引用就足够了。

但是,如果您要在程序中传递结构体,则会通过值传递整个结构体的副本,除非您明确指定按引用传递(使用'Ref'关键字)。


我非常确定复制结构体仍然只会复制引用,而不是包含的对象。 - Daniel Hume
@DanielHume 的确如此,但由于您存储的只是引用,所以您只需要复制引用即可。您不希望复制整个对象,因为这可能非常大且代价高昂。GameObject 是一个类,因此定义为其类型的变量都将是引用而不是实际对象。 - Graeme
@DanielHume 如果你指的是结构体本身的引用,那么不,结构体是字面类型,因此除非使用Ref,否则会按值传递。测试一下,创建一个带有int的结构体,打印它的值,将其传递给一个方法并增加该值,然后再次打印该值,你会发现原始结构体中的值没有增加,只有方法内部的副本增加了。 - Graeme
所以我们达成了一致。你可能想要重新表述一下你最后一段的内容,因为短语“然而,它将按值传递”似乎暗示着GameObject将会按值传递,连同所有其他结构字段一起。 - Daniel Hume
@DanielHume 哈哈!我重新措辞了,这是一个“在我的脑海中非常合理”的时刻 :) 你发现得好。 - Graeme

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接