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

有没有人知道答案和/或有一个看法呢?

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

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

我个人认为这是一个错误。 超过4个字段的元组是非常不寻常的,无论如何都应该用一个更有代表性的替代方法来取代(比如F#中的loggingtypes),所以只有小元组才具有实际意义。 我自己的基准testing表明,512字节的unboxed元组仍然可能比boxed元组更快。

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

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

元组通常出现这些性能问题的真实情况是使用元组作为字典中的关键字。 我实际上偶然遇到了这个线程,通过下面的链接从堆栈溢出问题F#运行我的algorithm比Python慢​​! 在那里作者的F#程序竟然比他的Python慢​​,正是因为他使用了盒装元组。 使用手写structtypes手动拆箱使得他的F#程序比Python快几倍。 如果元组由值types表示而不是以引用types开始,那么这些问题就不会出现。

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

而不是让一些元组成为值types,而另一些元组则是引用types,并强迫开发人员知道哪些是我想象的那些微软的人认为使所有引用types更简单。

啊,怀疑证实! 请参阅构build元组 :

第一个重大决定是将元组作为参考还是值types处理。 由于任何时候你想改变一个元组的值都是不变的,所以你必须创build一个新的元组。 如果它们是引用types,这意味着如果要在紧凑循环中更改元组中的元素,则可能会产生大量垃圾。 F#元组是参考types,但是团队中有一种感觉,如果两个或者三个元组元素是值types,他们可以实现性能改进。 一些创build了内部元组的团队使用了值而不是引用types,因为他们的场景对创build大量的pipe理对象非常敏感。 他们发现使用值types给了他们更好的performance。 在我们的元组规范的初稿中,我们将二元,三元和四元元组保留为值types,其余为引用types。 然而,在包括其他语言代表在内的devise会议上,由于这两种types的语义略有不同,所以决定这种“分离”devise会令人困惑。 在行为和devise上的一致性被确定为比潜在的性能增加更高的优先级。 基于这个input,我们改变了devise,使所有元组都是引用types,尽pipe我们要求F#团队进行一些性能调查,以查看对于某些元组大小使用值types时是否经历了加速。 它有一个很好的方法来testing这个,因为用F#编写的编译器就是在各种场景中使用元组的大型程序的一个很好的例子。 最后,F#团队发现,当某些元组是值types而不是引用types时,并没有得到性能改进。 这使我们对使用元组引用types的决定感到更好。

如果.NET System.Tuple <…>types被定义为结构体,它们将不可伸缩。 例如,一个长整数的三元组目前比例如下:

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

如果三元组被定义为一个结构,结果如下(基于我实现的一个testing例子):

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

由于元组在F#中具有内置的语法支持,并且在这种语言中它们经常被使用,所以“struct”元组将会使F#程序员在没有意识到的情况下编写低效率的程序。 这很容易发生:

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

在我看来,“结构”元组会在日常编程中造成很大的低效率。 另一方面,现有的“类”元组也会导致一定的低效率,正如@Jon所提到的那样。 但是,我认为,“发生概率”乘以“潜在损害”的结果,在结构上要比现在的阶级要高得多。 因此,目前的实施是较轻的罪恶。

理想情况下,将会有“类”元组和“结构”元组,这两个元组都有F#中的语法支持!

编辑(2017-10-07)

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

  • 内置到mscorlib(.NET> = 4.7)作为System.ValueTuple
  • 可用作其他版本的NuGet
  • C#>中的语法支持= 7
  • F#>中的语法支持= 4.1

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

对马特·埃利斯(Matt Ellis)文章的一个小小的澄清是,当不变性生效(当然这将是这种情况)时,参考和值types之间的使用语义的差别仅仅是“轻微的”。 尽pipe如此,我认为在BCLdevise中最好不要引入Tuple在某个阈值上交叉引用types的混淆。

我不知道,但如果你曾经使用F#元组是语言的一部分。 如果我创build了一个.dll文件,并且返回了一个元组types,那么最好有一个types来放置它。现在我怀疑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)