为什么.Net 4.0中的新Tuple类型是引用类型(类)而不是值类型(struct)

时间:2010-03-09 16:40:05

标签: performance class struct .net-4.0

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

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

5 个答案:

答案 0 :(得分:89)

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

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

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

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

通常出现元组性能问题的真实情况是使用元组作为字典中的键。我实际上是通过跟踪Stack Overflow问题F# runs my algorithm slower than Python!中的一个链接来发现这个线程的,其中作者的F#程序结果比他的Python慢​​,因为他使用的是盒装元组。使用手写struct类型手动取消装箱使他的F#程序比Python快几倍,速度更快。如果元组由值类型表示而不是以...开头的引用类型,则永远不会出现这些问题。

答案 1 :(得分:43)

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

不是让一些元组成为值类型而其他元素是引用类型,并迫使开发人员知道哪些是我想象的,微软的人认为使它们都是引用类型更简单。

啊,怀疑确认了!请参阅Building Tuple

  

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

答案 2 :(得分:7)

如果将.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#中具有内置语法支持,并且它们在这种语言中经常使用,“struct”元组会使F#程序员面临编写低效程序的风险,甚至不知道它。这很容易发生:

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

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

理想情况下,会有“class”元组和“struct”元组,两者都在F#中具有语法支持!

编辑(2017-10-07)

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

答案 3 :(得分:4)

对于2元组,您仍然可以始终使用KeyValuePair&lt; TKey,TValue&gt;从早期版本的Common Type System。这是一种价值类型。

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

答案 4 :(得分:0)

我不知道,但如果你曾经使用过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)