【问题标题】:Why is the new Tuple type in .Net 4.0 a reference type (class) and not a value type (struct)为什么 .Net 4.0 中的新 Tuple 类型是引用类型(类)而不是值类型(结构)
【发布时间】:2011-01-25 12:37:09
【问题描述】:

有人知道答案和/或对此有意见吗?

由于元组通常不会很大,我认为使用结构比使用类更有意义。你说什么?

【问题讨论】:

  • 对于 2016 年之后在这里绊倒的任何人。在 c# 7 及更高版本中,元组文字属于 ValueTuple<...> 类型家族。参考C# tuple types

标签: performance class struct .net-4.0


【解决方案1】:

为了简单起见,Microsoft 使所有元组类型都引用类型。

我个人认为这是一个错误。具有超过 4 个字段的元组非常不寻常,无论如何都应该用更具类型的替代方案(例如 F# 中的记录类型)替换,因此只有小的元组才具有实际意义。我自己的基准测试表明,高达 512 字节的未装箱元组仍然比装箱元组更快。

虽然内存效率是一个问题,但我认为主要问题是 .NET 垃圾收集器的开销。 .NET 上的分配和收集非常昂贵,因为它的垃圾收集器没有经过非常严格的优化(例如,与 JVM 相比)。此外,默认的 .NET GC(工作站)尚未并行化。因此,使用元组的并行程序停止运行,因为所有内核都在争夺共享垃圾收集器,从而破坏了可伸缩性。这不仅是主要的问题,而且 AFAIK 在他们检查这个问题时完全被微软忽略了。

另一个问题是虚拟调度。引用类型支持子类型,因此,它们的成员通常通过虚拟调度调用。相反,值类型不支持子类型,因此成员调用是完全明确的,并且始终可以作为直接函数调用执行。虚拟调度在现代硬件上非常昂贵,因为 CPU 无法预测程序计数器将在哪里结束。 JVM 竭尽全力优化虚拟调度,但 .NET 却没有。但是,.NET 确实以值类型的形式提供了对虚拟分派的转义。因此,将元组表示为值类型可以再次显着提高性能。例如,在 2 元组上调用 GetHashCode 一百万次需要 0.17 秒,但在等效结构上调用它只需要 0.008 秒,即值类型比引用类型快 20 倍。

通常会出现这些元组性能问题的实际情况是使用元组作为字典中的键。实际上,我通过 Stack Overflow 问题F# runs my algorithm slower than Python! 的链接偶然发现了这个线程,作者的 F# 程序结果比他的 Python 慢,正是因为他使用的是盒装元组。使用手写的struct 类型手动拆箱使他的 F# 程序快几倍,并且比 Python 快。如果元组是由值类型而不是引用类型来表示的,那么这些问题就不会出现......

【讨论】:

  • @Bent:是的,当我在 F# 的热路径上遇到元组时,我就是这样做的。如果他们在 .NET Framework 中同时提供了装箱和未装箱的元组,那就太好了……
  • 关于虚拟调度,我认为你的责任错了:Tuple<_,...,_> 类型可能已经被密封,在这种情况下,尽管是引用类型,但不需要虚拟调度。我更好奇为什么它们不是密封的而不是它们为什么是引用类型。
  • 根据我的测试,对于在一个函数中生成元组并返回到另一个函数,然后不再使用的场景,暴露字段结构似乎为 提供了卓越的性能任何 大小的数据项,它不会大到炸毁堆栈。不可变类只有在引用被传递到足以证明它们的构造成本合理的情况下才会更好(数据项越大,为了权衡取舍而需要传递的次数就越少)。由于元组应该只是表示一堆粘在一起的变量,因此结构体似乎是理想的。
  • “最多 512 字节的未装箱元组仍可能比装箱更快” - 这是哪种情况?您可能能够比持有 512B 数据的类实例更快地分配 512B 结构,但传递它会慢 100 倍以上(假设 x86)。有什么我忽略的吗?
【解决方案2】:

原因很可能是因为只有较小的元组作为值类型才有意义,因为它们占用的内存很小。较大的元组(即具有更多属性的元组)实际上会降低性能,因为它们会大于 16 个字节。

与其让一些元组是值类型,而另一些是引用类型,并迫使开发人员知道哪些是我想微软的人认为将它们全部设为引用类型更简单。

啊,怀疑得到证实!请看Building Tuple:

第一个重大决定是是否 将元组视为参考 或值类型。既然他们是 在您想要更改的任何时候都不可变 元组的值,你必须 创建一个新的。如果它们是 引用类型,这意味着可以 如果你会产生很多垃圾 正在更改 a 中的元组中的元素 紧密的循环。 F# 元组被引用 类型,但有一种感觉 他们可以实现的团队 如果两个,则性能改进,并且 也许三个,元素元组是 取而代之的是值类型。一些球队 已创建内部元组已使用 值而不是引用类型, 因为他们的场景非常 对创建大量托管的敏感 对象。他们发现使用一个值 type 给了他们更好的性能。在 我们的元组初稿 规范,我们保留了两个, 三元素和四元素元组为 值类型,其余为 引用类型。然而,在一个 设计会议包括 其他语言的代表 决定这次“分裂” 设计会令人困惑,因为 之间的语义略有不同 两种类型。行为的一致性 和设计被确定为 优先级高于潜在 性能增加。基于此 输入,我们改变了设计,使 所有元组都是引用类型, 虽然我们要求 F# 团队做 一些性能调查,看看 如果它在使用时经历了加速 某些大小的元组的值类型。 它有一个很好的测试方法,因为 它的编译器是用 F# 编写的,是一个 一个大型程序的好例子 在各种场景中使用元组。 最后,F#团队发现它 没有得到性能提升 当一些元组是值类型时 而不是引用类型。这个使 我们对自己的决定感觉更好 使用元组的引用类型。

【讨论】:

  • 在这里进行精彩讨论:blogs.msdn.com/bclteam/archive/2009/07/07/…
  • 啊,我明白了。我仍然有点困惑,值类型在实践中没有任何意义:P
  • 我刚刚阅读了关于没有通用接口的评论,并且在之前查看代码时,这正是另一件让我印象深刻的事情。 Tuple 类型的不通用性真的很令人沮丧。但是,我想您总是可以自己制作...无论如何,C# 中没有语法支持。然而至少......不过,泛型的使用和它所具有的约束在.Net 中仍然受到限制。非常通用非常抽象的库有很大的潜力,但泛型可能需要额外的东西,比如协变返回类型。
  • 您的“16 字节”限制是假的。当我在 .NET 4 上对此进行测试时,我发现 GC 非常慢,以至于高达 512 字节的未装箱元组仍然可以更快。我也会质疑微软的基准测试结果。我敢打赌他们忽略了并行性(F# 编译器不是并行的),这就是避免 GC 真正得到回报的地方,因为 .NET 的工作站 GC 也不是并行的。
  • 出于好奇,我想知道编译器团队是否测试了将元组制作为 EXPOSED-FIELD 结构的想法?如果一个人有一个具有各种特征的类型的实例,并且需要一个除了一个不同的特征之外都相同的实例,那么暴露字段结构可以比任何其他类型更快地完成,并且随着结构变大,优势只会越来越大。
【解决方案3】:

如果 .NET System.Tuple<...> 类型被定义为结构,它们将不可扩展。例如,长整数的三元组目前按如下方式缩放:

type Tuple3 = System.Tuple<int64, int64, int64>
type Tuple33 = System.Tuple<Tuple3, Tuple3, Tuple3>
sizeof<Tuple3> // Gets 4
sizeof<Tuple33> // Gets 4

如果将三元组定义为结构体,结果如下(基于我实现的一个测试示例):

sizeof<Tuple3> // Would get 32
sizeof<Tuple33> // Would get 104

由于元组在 F# 中具有内置的语法支持,并且它们在这种语言中非常常用,因此“结构”元组会使 F# 程序员面临编写效率低下的程序的风险,而他们甚至都没有意识到这一点。这很容易发生:

let t3 = 1L, 2L, 3L
let t33 = t3, t3, t3

在我看来,“结构”元组很可能会在日常编程中造成显着的低效率。另一方面,正如@Jon 所提到的,当前存在的“类”元组也会导致某些低效率。但是,我认为结构的“发生概率”乘以“潜在损害”的乘积要比当前的类高得多。因此,当前的实现是较小的邪恶。

理想情况下,“类”元组和“结构”元组都在 F# 中具有语法支持!

编辑 (2017-10-07)

现在完全支持结构元组,如下所示:

【讨论】:

  • 如果避免不必要的复制,any 大小的暴露字段结构将比相同大小的不可变类更有效,除非每个实例被复制足够多的次数这种复制的成本超过了创建堆对象的成本(盈亏平衡的副本数量随对象大小而变化)。如果一个人想要一个假装不可变的结构,这种复制可能是不可避免的,但是设计为显示为变量集合的结构(这是结构 )即使它们很大也可以有效地使用.
  • 这可能是 F# 不能很好地使用 ref 传递结构的想法,或者可能不喜欢所谓的“不可变结构”不是这样的事实,尤其是在装箱时。太糟糕了 .net 从未实现通过可强制执行的const ref 传递参数的概念,因为在许多情况下,真正需要的是这样的语义。
  • 顺便说一下,我认为GC的摊销成本是分配对象成本的一部分;如果在每兆字节分配后需要进行 L0 GC,那么分配 64 字节的成本大约是 L0 GC 成本的 1/16,000,加上任何 L1 或 L2 GC 成本的一小部分,这些成本成为必要的结果。
  • “我认为结构的发生概率乘以潜在损害的乘积会比当前的类高得多”。 FWIW,我很少在野外看到元组的元组并将它们视为设计缺陷,但我经常看到人们在使用(参考)元组作为Dictionary 中的键时遇到糟糕的性能,例如这里:stackoverflow.com/questions/5850243/…
  • @Jon 我写这个答案已经两年了,我现在同意你的观点,如果至少 2 元组和 3 元组是结构,那将是更好的选择。 F#语言用户语音suggestion已经在这方面做了。这个问题有一定的紧迫性,因为近年来大数据、量化金融和游戏领域的应用程序大量增长。
【解决方案4】:

对于 2 元组,您仍然可以始终使用公共类型系统早期版本中的 KeyValuePair。这是一个值类型。

对 Matt Ellis 文章的一个小的澄清是,引用类型和值类型之间的使用语义差异仅在不变性生效时“轻微”(当然,这里就是这种情况)。尽管如此,我认为最好不要在 BCL 设计中引入让 Tuple 在某个阈值处交叉到引用类型的混淆。

【讨论】:

  • 如果一个值在返回后将被使用一次,那么任何大小的暴露字段结构都将优于任何其他类型,前提是它不会大到炸毁堆栈。只有当引用最终被多次共享时,构建类对象的成本才会得到补偿。有时,将通用的固定大小异构类型作为类很有用,但有时结构会更好——即使对于“大”事物也是如此。
  • 感谢您添加这个有用的经验法则。然而,我希望你没有误解我的立场:我是一个价值型的瘾君子。 (stackoverflow.com/a/14277068 应该毫无疑问)。
  • 值类型是 .net 的一大特点,但不幸的是,写 msdn dox 的人没有认识到它们有多个不相交的用例,不同的用例应该有不同的指导方针。 msdn 推荐的 struct 样式只能与表示同质值的结构一起使用,但是如果需要表示一些用胶带固定在一起的独立值,则不应使用 that 结构风格——一个应该使用带有暴露公共字段的结构。
【解决方案5】:

我不知道,但如果您曾经使用过 F#,元组是该语言的一部分。如果我创建了一个 .dll 并返回了一种类型的元组,那么最好有一个类型来放入它。我现在怀疑 F# 是语言的一部分(.Net 4),对 CLR 进行了一些修改以适应一些常见的结构在 F# 中

来自http://en.wikibooks.org/wiki/F_Sharp_Programming/Tuples_and_Records

let scalarMultiply (s : float) (a, b, c) = (a * s, b * s, c * s);;

val scalarMultiply : float -> float * float * float -> float * float * float

scalarMultiply 5.0 (6.0, 10.0, 20.0);;
val it : float * float * float = (30.0, 50.0, 100.0)

【讨论】:

    猜你喜欢
    • 2011-12-06
    • 2011-11-09
    • 2017-03-26
    • 1970-01-01
    • 1970-01-01
    • 2020-11-21
    • 1970-01-01
    • 1970-01-01
    • 2018-10-11
    相关资源
    最近更新 更多