【问题标题】:When is it more efficient to pass structs by value and when by ref in C#?在 C# 中,什么时候按值传递结构,什么时候按 ref 传递结构更有效?
【发布时间】:2017-09-19 04:28:14
【问题描述】:

我进行了一些研究,似乎普遍认为结构应该小于 16 个字节,否则它们会因复制而导致性能损失。使用 C#7 和 ref return,完全避免复制结构变得非常容易。我假设随着结构体的大小变小,通过 ref 传递比复制值有更多的开销。

当按值传递结构比按引用更快时,是否有经验法则?哪些因素会影响这一点? (结构体大小、进程位数等)

更多上下文

我正在开发一款游戏,其中绝大多数数据表示为连续的结构数组,以实现最大的缓存友好性。正如您可能想象的那样,在这种情况下传递结构是很常见的。我知道分析是确定某事物的性能影响的唯一真正方法。但是,我想了解它背后的理论概念,并希望在编写代码时考虑到这种理解并仅分析边缘情况。

另外,请注意,我不是在询问最佳实践或通过 ref 传递所有内容的合理性。我知道“最佳做法”和影响,我故意选择不遵循它们。

解决“重复”标签

Performance of pass by value vs. pass by reference in C# .NET - 这个问题讨论了通过 ref 传递引用类型,这与我的要求完全不同。

In .Net, when if ever should I pass structs by reference for performance reasons? - 第二个问题有点触及主题,但它与结构的特定大小有关。

回答Eric Lippert's article的问题:

你真的需要回答这个问题吗?是的,我需要。因为它会影响我写很多代码的方式。

这真的是瓶颈吗?可能不是。但我仍然想知道,因为这是 99% 的程序的数据访问模式。在我看来,这类似于选择正确的数据结构。

差异是否相关?是的。通过 ref 传递大型结构更快。我只是想了解它的局限性。

您所说的“更快”是什么意思?就像为同一任务减少 CPU 的工作量一样。

你在看大局吗?是的。如前所述,它会影响我写整个东西的方式。

我知道我可以测量很多不同的组合。那告诉我什么?在我的 [.NET 版本、进程位数、操作系统、CPU] 的组合中,X 比 Y 快。 Linux 呢?安卓呢? iOS 呢?我应该对所有可能的硬件/软件组合的所有排列进行基准测试吗?

我认为这不是一个可行的策略。因此,我在这里问,希望对 CLR/JIT/ASM/CPU 有很多了解的人可以告诉我它是如何工作的,这样我就可以在编写代码时做出明智的决定。

我正在寻找的答案类似于前面提到的 16 字节结构大小指南,并解释了原因。

【问题讨论】:

  • 第二个问题的答案归结为何时使用结构,何时不使用。我有一个真实的场景,我的几乎所有数据都表示为大小差异很大的结构。
  • 你显然完全没有抓住重点。我没有复制任何东西,因为我是通过 ref 传递和返回的。
  • 我的问题中标记为粗体的部分有什么含义?我敢肯定,了解 CLR 和 JITter 内部工作原理的人可以用 2 句话明确回答。问题的其余部分旨在描述我的场景与您所链接的问题中的场景有何不同。
  • 这只是你的意见。因为我仍然想得到我的答案——我有什么选择?既然已经结束,我怎样才能让更了解该主题的人看到这个问题?编辑它?举报?
  • 这实际上是一个相关的问题。对于 99% 的程序,我认为这完全是愚蠢的——但如果你做一个内部游戏循环,或者即交易后端的自动收报机,这就是真正出现并产生差异的问题类型。如果您考虑跨度,它会变得更加相关,这样您就可以在不复制的情况下移动视图/数组的一部分。我个人有一些程序,其中核心循环大约是 3 页代码,使用 95% 的处理时间和 - 运行循环更新数组中的值,这些值出于性能原因表示为结构。好问题。

标签: c# performance struct ref


【解决方案1】:

一般来说,通过引用传递应该更快。
当您通过引用传递一个结构时,您只是传递一个指向该结构的指针,它只是一个 32/64 位整数。
当您通过值传递结构时,您需要复制整个结构,然后将指针传递给新副本。
除非结构非常小,例如 int,否则通过引用传递会更快。

此外,按值传递会增加调用操作系统进行内存分配和解除分配的次数,这些调用非常耗时,因为操作系统必须检查注册表的可用空间。

【讨论】:

  • 澄清一下,在这种情况下“非常小”=“小于设备上的指针”
  • @LLSv2.0 是的,尽管在某些情况下您仍希望通过值传递以使您的代码更易于使用,但这确实取决于具体情况
  • 需要更多经验数据。传递“指针”需要额外的间接传递,因此(取决于实现)除了这个简单的通用性之外,还有一些切入点。这个拐点是什么,在哪里?它如何/为什么在不同环境中有所不同?
  • @Riggy 根据Microsoft,“值类型在通过引用传递时没有装箱。”
  • 另外,正如@user2864740 所说——我问的是拐点。 1 int 更快,我同意。 2个整数呢? 3? 4?
【解决方案2】:

如果你通过引用传递结构,那么它们可以是任意大小。您仍在处理 8(假定为 x64)字节指针。为了获得最高性能,您需要一种 CPU 缓存友好型设计,称为数据驱动设计。

游戏经常使用一种特殊的数据驱动设计,称为实体组件系统。请参阅 Konrad Kokosa 所著的Pro .NET 内存管理一书第 14 章。

基本思想是您可以更新您的游戏实体,例如Movable, Car, Plane, ... 共享公共属性,例如存储在连续数组中的所有实体的位置。如果您需要增加 1K 个实体的位置,您只需要查找所有实体的位置数组的数组索引并在那里更新它们。这提供了最好的数据局部性。如果所有都存储在类中,CPU 预取器将被每个类实例的许多 this 指针丢失。

请参阅这篇有关参考架构的英特尔帖子:https://software.intel.com/en-us/articles/get-started-with-the-unity-entity-component-system-ecs-c-sharp-job-system-and-burst-compiler

那里有很多实体组件系统,但到目前为止,我还没有看到使用 ref 结构作为其主要工作数据结构。原因是所有流行的都比引入 ref 结构的 C# 7.2 存在的时间长得多。

【讨论】:

  • 以ECS为核心架构,正是我当初提出这个问题的原因。无论如何,如果数据不是存储为结构数组而是作为类数组存储,那么数据很可能会分布在堆中并且不会以任何方式连续,因为类数组实际上是指向的指针数组堆
  • @loodakrawa:您是否使用 C# 7.2 功能完成了您的 ECS 系统?它是开源的吗?将您的与 Entitas 等其他产品进行比较会很有趣。
  • 我在大约 3 年前实现了它,所以我没有使用 7.2 的功能。从那以后我就不再尝试制作游戏了,但我现在又开始玩了,所以我可能会用新的好东西来升级它。无论如何,你可以在这里查看:github.com/loodakrawa/ScatteredLogic
  • 这里,带有 ref 结构的开源 ECS 框架:LeoECS。速度快,不仅适用于游戏。享受那种速度 =)
【解决方案3】:

我终于找到了答案。断点是 System.IntPtr.Size。用微软自己的话来自Write safe and efficient C# code

添加 in 修饰符以通过引用传递参数,并声明您的设计意图通过引用传递参数以避免不必要的复制。您不打算修改用作该参数的对象。

这种做法通常会提高大于 IntPtr.Size 的只读值类型的性能。对于简单类型(sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、decimal 和 bool 以及 enum 类型),任何潜在的性能提升都是最小的。事实上,对小于 IntPtr.Size 的类型使用传递引用可能会降低性能。

【讨论】:

    猜你喜欢
    • 2015-09-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-03-04
    • 2011-02-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多