【发布时间】:2011-01-25 12:37:09
【问题描述】:
有人知道答案和/或对此有意见吗?
由于元组通常不会很大,我认为使用结构比使用类更有意义。你说什么?
【问题讨论】:
-
对于 2016 年之后在这里绊倒的任何人。在 c# 7 及更高版本中,元组文字属于
ValueTuple<...>类型家族。参考C# tuple types
标签: performance class struct .net-4.0
有人知道答案和/或对此有意见吗?
由于元组通常不会很大,我认为使用结构比使用类更有意义。你说什么?
【问题讨论】:
ValueTuple<...> 类型家族。参考C# tuple types
标签: performance class struct .net-4.0
为了简单起见,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 快。如果元组是由值类型而不是引用类型来表示的,那么这些问题就不会出现......
【讨论】:
Tuple<_,...,_> 类型可能已经被密封,在这种情况下,尽管是引用类型,但不需要虚拟调度。我更好奇为什么它们不是密封的而不是它们为什么是引用类型。
原因很可能是因为只有较小的元组作为值类型才有意义,因为它们占用的内存很小。较大的元组(即具有更多属性的元组)实际上会降低性能,因为它们会大于 16 个字节。
与其让一些元组是值类型,而另一些是引用类型,并迫使开发人员知道哪些是我想微软的人认为将它们全部设为引用类型更简单。
啊,怀疑得到证实!请看Building Tuple:
第一个重大决定是是否 将元组视为参考 或值类型。既然他们是 在您想要更改的任何时候都不可变 元组的值,你必须 创建一个新的。如果它们是 引用类型,这意味着可以 如果你会产生很多垃圾 正在更改 a 中的元组中的元素 紧密的循环。 F# 元组被引用 类型,但有一种感觉 他们可以实现的团队 如果两个,则性能改进,并且 也许三个,元素元组是 取而代之的是值类型。一些球队 已创建内部元组已使用 值而不是引用类型, 因为他们的场景非常 对创建大量托管的敏感 对象。他们发现使用一个值 type 给了他们更好的性能。在 我们的元组初稿 规范,我们保留了两个, 三元素和四元素元组为 值类型,其余为 引用类型。然而,在一个 设计会议包括 其他语言的代表 决定这次“分裂” 设计会令人困惑,因为 之间的语义略有不同 两种类型。行为的一致性 和设计被确定为 优先级高于潜在 性能增加。基于此 输入,我们改变了设计,使 所有元组都是引用类型, 虽然我们要求 F# 团队做 一些性能调查,看看 如果它在使用时经历了加速 某些大小的元组的值类型。 它有一个很好的测试方法,因为 它的编译器是用 F# 编写的,是一个 一个大型程序的好例子 在各种场景中使用元组。 最后,F#团队发现它 没有得到性能提升 当一些元组是值类型时 而不是引用类型。这个使 我们对自己的决定感觉更好 使用元组的引用类型。
【讨论】:
如果 .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)
现在完全支持结构元组,如下所示:
【讨论】:
ref 传递结构的想法,或者可能不喜欢所谓的“不可变结构”不是这样的事实,尤其是在装箱时。太糟糕了 .net 从未实现通过可强制执行的const ref 传递参数的概念,因为在许多情况下,真正需要的是这样的语义。
Dictionary 中的键时遇到糟糕的性能,例如这里:stackoverflow.com/questions/5850243/…
对于 2 元组,您仍然可以始终使用公共类型系统早期版本中的 KeyValuePair
对 Matt Ellis 文章的一个小的澄清是,引用类型和值类型之间的使用语义差异仅在不变性生效时“轻微”(当然,这里就是这种情况)。尽管如此,我认为最好不要在 BCL 设计中引入让 Tuple 在某个阈值处交叉到引用类型的混淆。
【讨论】:
我不知道,但如果您曾经使用过 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)
【讨论】: