为什么.Net 4.0中的新元组类型是引用类型(类)而不是值类型(结构)


Answers:


94

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

我个人认为这是一个错误。具有四个以上字段的元组是非常不寻常的,无论如何都应使用类型更丰富的替代项(例如F#中的记录类型)替换,因此只有较小的元组才有意义。我自己的基准测试显示,最多512字节的未装箱元组可能仍比装箱元组更快。

尽管内存效率是一个令人关注的问题,但我相信主要的问题是.NET垃圾收集器的开销。在.NET上,分配和收集非常昂贵,因为其垃圾收集器尚未进行非常严格的优化(例如,与JVM相比)。此外,默认的.NET GC(工作站)尚未并行化。因此,使用元组的并行程序由于所有内核争用共享垃圾收集器而停止运行,从而破坏了可伸缩性。这不仅是最主要的问题,而且AFAIK在他们研究此问题时被Microsoft完全忽略。

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

通常在元组中出现这些性能问题的实际情况是使用元组作为字典中的键。我实际上是通过跟踪来自堆栈溢出问题F#的链接偶然发现此线程的,它的算法运行速度比Python慢​​!作者的F#程序实际上比他的Python慢​​,因为他使用的是盒装元组。使用手写struct类型手动取消装箱,使他的F#程序快了好几倍,并且比Python快。如果元组以值类型而不是引用类型表示,则永远不会出现这些问题。


2
@Bent:是的,这正是我在F#的热路径上遇到元组时所做的事情。如果他们在.NET Framework中同时提供了已装箱和未装箱的元组,那就太好了……
JD

18
关于虚拟调度,我认为您的责任错了:Tuple<_,...,_>类型可能已经被密封,在这种情况下,尽管是引用类型,也不需要虚拟调度。我比它们为什么是引用类型更好奇为什么它们没有被密封。
kvb 2012年

2
根据我的测试,对于一个元组将在一个函数中生成并返回到另一个函数,然后不再使用的情况,对于任何大小不大但不会造成打击的大小数据项,暴露场结构似乎都提供了卓越的性能堆栈。不可变的类只有在充分传递引用以证明其构造成本合理的情况下才更好(数据项越大,为获得折衷而需要对其进行传递的次数就越少)。由于元组应该只代表一堆变量,所以结构似乎是理想的。
2013年

2
“最大为512字节的未装箱元组仍可能比装箱的元组更快” -那是哪种情况?您也许可以比拥有512B数据的类实例分配512B结构更快,但是传递它的速度将慢100倍以上(假定x86)。我有什么要注意的吗?
Groo 2014年


45

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

而不是让某些元组作为值类型,而让其他元组作为引用类型,而迫使开发人员知道哪些是元组,我可以想象微软的人认为使它们成为所有引用类型更为简单。

啊,怀疑得到证实!请参阅建立元组

第一个主要决定是将元组视为参考还是值类型。由于只要您想更改元组的值,它们都是不可变的,因此您必须创建一个新的元组。如果它们是引用类型,则意味着如果在紧密循环中更改元组中的元素,可能会产生大量垃圾。F#元组是引用类型,但是团队认为,如果两个(也许三个)元素元组是值类型,它们可以实现性能改进。一些创建内部元组的团队使用了值而不是引用类型,因为他们的场景对创建很多托管对象非常敏感。他们发现使用值类型可以提高性能。在元组规范的初稿中,我们将二元,三元和四元元组保留为值类型,其余为引用类型。但是,在包括来自其他语言的代表的设计会议中,由于两种类型之间的语义略有不同,因此决定这种“拆分”设计会造成混淆。行为和设计的一致性被确定为比潜在性能提高更高的优先级。基于此输入,我们更改了设计,以使所有元组均为引用类型,尽管我们要求F#团队进行一些性能调查,以查看将值类型用于某些大小的元组时是否经历了加速。它有一个很好的测试方法,因为它的编译器是用F#编写的,这是一个大型程序的好例子,该程序在各种情况下都使用了元组。最后,F#团队发现,当某些元组是值类型而不是引用类型时,它并没有获得性能上的提高。这使我们对将引用类型用于元组的决定感到更好。



啊,我明白了。我仍然有些困惑,值类型在这里实际上并不代表任何东西:P
Bent Rasmussen

我只是阅读了有关没有通用接口的评论,而在较早地看代码时,这确实是令我着迷的另一件事。真正令人鼓舞的是元组类型的泛型。但是,我想您总是可以自己做的……反正C#中没有语法支持。至少,至少.... net仍然对泛型的使用及其约束感到有限。非常通用的非常抽象的库有很大的潜力,但是通用可能需要额外的东西,例如协变返回类型。
Bent Rasmussen 2010年

7
您的“ 16字节”限制是虚假的。当我在.NET 4上进行测试时,我发现GC太慢了,以至于最多512字节的拆箱元组仍然可以更快。我也会质疑微软的基准测试结果。我敢打赌他们忽略了并行性(F#编译器不是并行的),这就是避免GC真正奏效的地方,因为.NET的工作站GC也不是并行的。
JD

出于好奇,我想知道编译器团队是否测试了使元组成为EXPOSED-FIELD结构的想法?如果一个人有各种性状的类型的实例,并需要一个实例,它是除了一个特点是不同的相同,露出的场结构能够做到这快于任何其他类型的,人和的优势只生长的结构GET大。
supercat 2012年

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#中具有内置的语法支持,并且在该语言中使用频率很高,因此“结构”元组将使F#程序员面临编写效率低下的程序而没有意识到的风险。这很容易发生:

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

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

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

编辑(2017-10-07)

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


2
如果避免不必要的复制,则任何大小的公开字段结构都将比相同大小的不可变类更有效,除非每个实例被复制足够的次数以至于这种复制的开销克服了创建堆对象的开销(收支平衡的副本数随对象大小而变化)。如果一个人想它伪装成一个永恒不变的结构,但其目的是显示为变量的集合(这是什么结构结构这样的复制可能是不可避免),可即使他们是巨大的有效利用。
2012年

2
F#可能不能很好地与通过传递结构的想法配合使用ref,或者可能不喜欢所谓的“不可变结构”的事实,尤其是在装箱时。太糟糕了。.net从未实现过通过可执行文件传递参数的概念const ref,因为在很多情况下,这种语义是真正需要的。
2012年

1
顺便说一句,我认为GC的摊销成本是分配对象成本的一部分。如果在每兆字节分配之后需要L0 GC,则分配64个字节的成本大约是L0 GC成本的1 / 16,000,再加上一部分L1或L2 GC的成本的一部分结果。
2012年

4
“我认为结构的发生概率乘以潜在破坏的乘积将比当前使用类高得多。” FWIW,我很少在野外看到元组,并认为它们是设计缺陷,但是我经常看到人们在使用(ref)元组作为键时,人们在糟糕的性能上苦苦挣扎。Dictionary,例如:stackoverflow.com/questions/5850243 /…
JD

3
@Jon我写了这个答案已经两年了,现在我同意你的观点,如果至少2和3元组是结构,那将是可取的。在这方面已经提出了F#语言用户语音建议。由于近年来在大数据,量化金融和游戏领域的应用大量增长,因此该问题具有紧迫性。
Marc Sigrist 2014年

4

对于2元组,您仍然可以始终使用早期版本的Common Type System中的KeyValuePair <TKey,TValue>。这是一个值类型。

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


如果一个值在返回后将被使用一次,则任何大小的暴露域结构都将胜过任何其他类型,只要它的大小不至于使堆栈崩溃那么大。仅当引用最终被多次共享时,才可以收回构建类对象的成本。有时将通用固定大小的异构类型用作类很有用,但是有时结构会更好-即使对于“大”事情也是如此。
2013年

感谢您添加此有用的经验法则。但是,我希望您不要误解我的立场:我是一个价值型迷。(stackoverflow.com/a/14277068毫无疑问)。
Glenn Slayden

值类型是.net的重要功能之一,但是不幸的是,编写msdn dox的人未能意识到它们存在多个不相交的用例,并且不同的用例应具有不同的准则。msdn建议的结构样式仅应与表示同一个值的结构一起使用,但是如果需要表示一些用胶带固定在一起的独立值,则不应使用结构样式,而应将结构样式与暴露的公共领域。
2013年

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)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.