.Net 4.0の新しいタプル型が値型(構造体)ではなく参照型(クラス)である理由


88

誰かが答えを知っているか、これについて意見を持っていますか?

タプルは通常それほど大きくないので、これらよりもクラスよりも構造体を使用する方が理にかなっていると思います。何て言うの?


1
2016年以降にここでつまずく人のために。c#7以降では、タプルリテラルはタイプファミリーValueTuple<...>です。C#タプルタイプの
Tamir Daniely

回答:


93

マイクロソフトは、単純化のために、すべてのタプルタイプを参照タイプにしました。

個人的にこれは間違いだったと思います。4つを超えるフィールドを持つタプルは非常に珍しいものであり、とにかくよりタイプフルな代替(F#のレコードタイプなど)に置き換える必要があるため、小さなタプルのみが実用的に重要です。私自身のベンチマークは、512バイトまでのボックス化されていないタプルは、ボックス化されたタプルよりも高速である可能性があることを示しました。

メモリ効率は1つの懸念事項ですが、主な問題は.NETガベージコレクタのオーバーヘッドにあると思います。ガベージコレクタは(JVMと比較して)あまり最適化されていないため、.NETでの割り当てとコレクションは非常に高価です。さらに、デフォルトの.NET GC(ワークステーション)はまだ並列化されていません。その結果、タプルを使用する並列プログラムは、すべてのコアが共有のガベージコレクターをめぐって争い、スケーラビリティを破壊するので、停止します。これは主要な懸念事項であるだけでなく、AFAIKは、Microsoftがこの問題を調査したときに完全に無視されました。

もう1つの懸念は、仮想ディスパッチです。参照型はサブタイプをサポートするため、それらのメンバーは通常、仮想ディスパッチを介して呼び出されます。対照的に、値型はサブタイプをサポートできないため、メンバーの呼び出しは完全に明確であり、常に直接関数呼び出しとして実行できます。CPUがプログラムカウンターが終了する場所を予測できないため、仮想ディスパッチは最新のハードウェアでは非常に高価です。JVMは仮想ディスパッチを最適化するために非常に長くなりますが、.NETはそうではありません。ただし、.NETは、値型の形式で仮想ディスパッチからの脱出を提供します。したがって、タプルを値型として表すと、ここでもパフォーマンスが劇的に向上する可能性があります。たとえば、GetHashCode 2タプルで100万回かかると0.17秒かかりますが、同等の構造体で呼び出すと0.008秒しかかかりません。つまり、値の型は参照型よりも20倍高速です。

タプルに関するこれらのパフォーマンスの問題が一般的に発生する実際の状況は、タプルを辞書のキーとして使用する場合です。Stack Overflowの質問のリンクをたどってこのスレッドに実際に遭遇しました。F#は私のアルゴリズムをPythonよりも低速で実行します。作者のF#プログラムは、ボックス化されたタプルを使用していたため、Pythonよりも遅いことがわかりました。手書きのstruct型を使用して手動でボックス化を解除すると、彼のF#プログラムがPythonより数倍速く、高速になります。これらの問題は、タプルが値の型で表され、最初に参照型ではない場合には発生しませんでした...


2
@ベント:はい、F#のホットパスでタプルに遭遇したとき、まさにそれを行います。それらは、.NET Frameworkで両方の箱入りとアンボクシングタプルを提供した場合はいいだろうけれども...
JD

18
仮想ディスパッチに関しては、あなたのせいは誤解Tuple<_,...,_>されていると思います。型は封印されている可能性があり、その場合、参照型であっても仮想ディスパッチは必要ありません。それらが参照型である理由よりも、なぜそれらがシールされないのかについて私はもっと興味があります。
kvb 2012

2
私のテストから、タプルが1つの関数に生成され、別の関数に戻って、再度使用されていないことになるというシナリオのために、露出フィールドの構造はのための優れた性能を提供するように見えるあらゆる打撃するように巨大ではありませんサイズのデータ項目をスタック。不変クラスは、構築コストを正当化するのに十分なだけ参照が渡される場合にのみ優れています(データアイテムが大きいほど、トレードオフのために渡される必要のある量が少なくなります)。タプルは一緒にスタックされた変数の束を単に表すことになっているので、構造体は理想的に見えるでしょう。
スーパーキャット2013

2
「512バイトまでのボックス化されていないタプルは、ボックス化よりも高速である可能性があります」 -どのシナリオですか?512Bのデータを保持するクラスインスタンスよりも速く512B構造体を割り当てることできる場合がありますが、それを渡すのは100倍以上遅くなります(x86を想定)。私が見落としているものはありますか?
Groo


45

その理由は、メモリフットプリントが小さいため、値の型として意味があるのは小さなタプルだけだからです。より大きなタプル(つまり、より多くのプロパティを持つタプル)は、16バイトよりも大きいため、実際にはパフォーマンスが低下します。

いくつかのタプルを値の型にして、他のタプルを参照の型にして、開発者にどれがどれであるかを知ってもらうよりも、Microsoftの人たちはすべての参照の型を作る方が簡単だと思ったと思います。

ああ、疑惑が確認されました!タプルの構築を参照してください:

最初の主要な決定は、タプルを参照タイプと値タイプのどちらとして扱うかでした。タプルの値を変更したいときはいつでも不変なので、新しいタプルを作成する必要があります。それらが参照型である場合、これは、タイトループでタプルの要素を変更している場合、大量のガベージが生成される可能性があることを意味します。F#のタプルは参照型でしたが、2つ、場合によっては3つの要素タプルを値型にすると、パフォーマンスの向上を実現できるとチームから感じていました。内部タプルを作成した一部のチームは、参照タイプではなく値を使用していました。彼らのシナリオは、多数の管理対象オブジェクトの作成に非常に敏感だからです。値型を使用すると、パフォーマンスが向上することがわかりました。タプル仕様の最初のドラフトでは、2要素、3要素、および4要素のタプルを値の型として保持し、残りは参照型です。ただし、他の言語の代表者を含む設計会議中に、2つのタイプのセマンティクスがわずかに異なるため、この「分割」設計は混乱を招くと判断されました。動作と設計の一貫性は、潜在的なパフォーマンスの向上よりも優先されると判断されました。この入力に基づいて、すべてのタプルが参照タイプになるように設計を変更しましたが、いくつかのサイズのタプルに値タイプを使用するときにスピードアップが発生するかどうかを確認するためにF#チームにパフォーマンス調査を依頼しました。これをテストする良い方法がありました。それは、F#で書かれたコンパイラーが、さまざまなシナリオでタプルを使用する大きなプログラムの良い例でした。最終的に、F#チームは、一部のタプルが参照型ではなく値型である場合、パフォーマンスが向上しないことを発見しました。これにより、タプルに参照型を使用するという決定について、私たちは気分が良くなりました。



ええ、わかりました。ここでも値の型が実際には何も意味しないというのは少し混乱しています:P
Bent Rasmussen

私は汎用インターフェースがないというコメントを読んだだけで、以前のコードを見ると、それはまさに私を驚かせた別のことでした。Tuple型がどれほど一般的でないかは、実に非常に刺激的ではありません。しかし、いつでも自分で作成できると思います...いずれにせよ、C#の構文サポートはありません。しかし、少なくとも...それでも、ジェネリックの使用とジェネリックの制約は、.Netでは制限されていると感じています。非常に一般的な非常に抽象的なライブラリにはかなりの可能性がありますが、ジェネリックにはおそらく共変の戻り値型などの追加のものが必要です。
Bent Rasmussen

7
「16バイト」の制限は偽です。これを.NET 4でテストしたところ、GCが非常に遅いため、最大512バイトのボックス化されていないタプルの方が高速であることがわかりました。また、マイクロソフトのベンチマーク結果についても質問します。私は彼らが並列処理を無視しているに違いない(F#コンパイラーは並列ではない)。
JD

好奇心から、タプルをEXPOSED-FIELD構造体にするというアイデアをコンパイラチームがテストしたのでしょうか。さまざまな特性を持つタイプのインスタンスがあり、1つの特性が異なることを除いて同一のインスタンスが必要な場合、公開フィールドstructは他のタイプよりもはるかに速く実行でき、構造体が取得するときにのみ利点が大きくなりますより大きい。
スーパーキャット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
不必要なコピーを回避する場合、各インスタンスが十分な回数コピーされて、そのようなコピーのコストがヒープオブジェクトの作成のコストを克服しない限り、任意のサイズの公開フィールド構造体が同じサイズの不変のクラスよりも効率的です(コピーの損益分岐点数はオブジェクトサイズによって異なります)。1は不変であることをふりますが、(構造体が何であるかの変数のコレクションとして表示されるように設計された構造体構造体たい場合、このようなコピーは避けられないかもしれあるが)、彼らは巨大である場合でも、効率的に使用することができます。
スーパーキャット2012年

2
F#はref、構造体をで渡すという考えにうまく対応していないか、いわゆる「不変の構造体」が特にボックス化されている場合はそうでないという事実を好まない可能性があります。const ref多くの場合、そのようなセマンティクスが実際に必要なものであるため、.netが実施可能なによってパラメーターを渡すという概念を実装していないのは残念です。
スーパーキャット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
@ジョンこの回答を書いてから2年が経過しましたが、少なくとも2と3のタプルが構造体であることが望ましいと私は今同意します。これに関して、F#言語のユーザーの声の提案が行われました。近年、ビッグデータ、定量金融、ゲームのアプリケーションが大幅に増加しているため、この問題にはいくつかの緊急性があります。
Marc Sigrist 2014年

4

2タプルの場合でも、以前のバージョンのCommon Type SystemのKeyValuePair <TKey、TValue>を常に使用できます。値型です。

Matt Ellisの記事を少し説明すると、参照と値の型の間の使用セマンティクスの違いは、不変性が有効な場合にのみ「ごくわずか」になることです(もちろん、ここでも同様です)。それでも、BCL設計では、タプルがあるしきい値で参照型にクロスオーバーするという混乱を招かないことが最善であったと思います。


値が返された後で1回使用される場合、スタックを爆破するほど巨大ではない場合に限り、任意のサイズの公開フィールド構造体が他のタイプよりも優れています。クラスオブジェクトを構築するコストは、参照が複数回共有される場合にのみ回収されます。汎用の固定サイズの異種混合型がクラスであると便利な場合もありますが、「大きな」ものであっても、構造体の方が優れている場合もあります。
スーパーキャット2013

この便利な経験則を追加していただきありがとうございます。しかし、あなたが私の立場を誤解していないことを願っています。私はバリュータイプのジャンキーです。(stackoverflow.com/a/14277068は間違いなく残るはずです)。
Glenn Slayden 2013

値の種類は.netの優れた機能の1つですが、残念ながら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)
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.