web-dev-qa-db-ja.com

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

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

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

87
Bent Rasmussen

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

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

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

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

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

92
Jon Harrop

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

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

ああ、疑惑が確認されました! Building Tuple を参照してください:

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

43
Andrew Hare

.NET System.Tuple <...> タイプが構造体として定義されている場合、それらはスケーラブルではありません。たとえば、長整数の3項タプルは現在次のようにスケーリングされます。

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

3項タプルが構造体として定義されている場合、結果は次のようになります(私が実装したテスト例に基づく)。

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)

構造タプルは、次のように完全にサポートされるようになりました。

7
Marc Sigrist

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

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

4
Glenn Slayden

わかりませんが、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)
0
Bionic Cyborg