web-dev-qa-db-ja.com

基本型(intなど)をクラスとして実装する場合の注意点は何ですか?

オブジェクト指向プログラミング言語を設計して提供する場合、ある時点で、基本型(intfloatdoubleまたは同等のもの)をクラスとして実装することを選択する必要がありますまたは、他の何か。明らかに、Cファミリの言語には、クラスとして定義する傾向がないがあります(Javaには特別なプリミティブ型があり、C#は不変の構造体としてそれらを実装します)。 。

基本的な型がクラスとして実装されている場合(階層が統一された型システムで)、これらの型はルート型の適切なLiskovサブタイプになり得るという、非常に重要な利点を考えることができます。したがって、ボクシング/アンボクシング(明示的または暗黙的)、ラッパータイプ、特殊な分散ルール、特殊な動作などで言語を複雑にすることは避けます。

もちろん、言語デザイナがその方法を決定する理由を部分的に理解できます。クラスインスタンスには空間オーバーヘッドがある傾向があります(インスタンスにはメモリレイアウトにvtableまたは他のメタデータが含まれている可能性があるため)。そのため、プリミティブ/構造体は必要ありません。 (言語がそれらの継承を許可しない場合)。

空間効率(および特に大規模な配列での空間局所性の向上)は、基本型がしばしばでないクラスである唯一の理由ですか?

私は一般的には答えを「はい」と想定しましたが、コンパイラにはエスケープ分析アルゴリズムがあり、インスタンス(基本タイプだけでなく任意のインスタンス)が厳密に証明されている場合、空間オーバーヘッドを(選択的に)省略できるかどうかを推定できます地元。

上記は間違っていますか、それとも他に何か不足していますか?

はい、それはほとんど効率に帰着します。しかし、その影響を過小評価しているようです(またはさまざまな最適化の効果を過大評価しているようです)。

まず、それは単なる「空間オーバーヘッド」ではありません。ボックス化/ヒープ割り当てされたプリミティブを作成すると、パフォーマンスコストも高くなります。これらのオブジェクトを割り当てて収集するというGCへの追加のプレッシャーがあります。 「プリミティブオブジェクト」が不変である場合、本来あるべきように、これは二重になります。次に、より多くのキャッシュミスが発生します(間接的なため、および指定された量のキャッシュに収まるデータが少ないため)。さらに、「オブジェクトのアドレスをロードしてから、そのアドレスから実際の値をロードする」には、「値を直接ロードする」よりも多くの指示が必要です。

第二に、エスケープ分析は妖精の粉よりも速くはありません。それは、まあ、エスケープしない値にのみ適用されます。ローカル計算(ループカウンターや計算の中間結果など)を最適化するのは確かに良いことであり、測定可能なメリットが得られます。しかし、はるかに大多数の値は、オブジェクトと配列のフィールドに住んでいます。確かに、それら自体がエスケープ分析の対象となる可能性がありますが、それらは通常変更可能な参照型であるため、それらのエイリアスはエスケープ分析に大きな課題を提示し、これらのエイリアス(1)はどちらもエスケープしないことを証明する必要があります、および(2)割り当てを排除する目的で違いを生まない。

任意のメソッド(ゲッターを含む)を呼び出すか、またはオブジェクトを引数としてany他のメソッドに渡すと、オブジェクトのエスケープに役立つので、プロシージャ間の分析が必要になります最も些細なケースを除いてすべて。これははるかに高価で複雑です。

そして、物事が本当にエスケープし、合理的に最適化できない場合があります。実際には、Cプログラマーがヒープ割り当ての問題をどれくらいの頻度で経験するかを考えれば、それらのかなりの数です。 intを含むオブジェクトがエスケープすると、エスケープ分析はintにも適用されなくなります。効率的なプリミティブfieldsに別れを告げます。

これは別の点に結びつきます。必要な分析と最適化は非常に複雑で、活発な研究分野です。言語の実装があなたが提案する最適化の程度を達成したかどうかは議論の余地があり、それが達成されたとしても、それはまれで非常に大きな努力でした。確かにこれらの巨人の肩の上に立つことは、自分自身が巨人になるよりは簡単ですが、それはまだ簡単なことではありません。最初の数年間は、競争力のあるパフォーマンスを期待しないでください。

それは、そのような言語が実行可能ではないということではありません。明らかにそうです。専用のプリミティブを使用する言語と同じくらい高速であるとは思わないでください。言い換えれば、 十分にスマートなコンパイラ のビジョンに惑わされないでください。

19
user7043

基本タイプがクラスではないことが多い理由は、空間効率(および特に大きな配列での空間局所性の向上)だけですか?

番号。

もう1つの問題は、基本型が基本演算で使用される傾向があることです。コンパイラーは、int + intが関数呼び出しにコンパイルされるのではなく、基本的なCPU命令(または同等のバイトコード)にコンパイルされることを知る必要があります。その時点で、intを通常のオブジェクトとして持っている場合は、とにかく効率的に箱を開ける必要があります。

これらの種類の操作も、サブタイピングでは実際にはうまくいきません。 CPU命令にディスパッチすることはできません。 from CPU命令をディスパッチすることはできません。つまり、サブタイピングの全体のポイントは、Dを使用できるBを使用できるようにすることです。 CPU命令はポリモーフィックではありません。プリミティブにそれを実行させるには、単純な追加(または何でも)として操作の数倍のコストがかかるディスパッチロジックで操作をラップする必要があります。 intを型階層の一部にすることの利点は、それが封印された/最終的なものである場合、少し問題になります。そして、それは二項演算子のディスパッチロジックで頭痛の種をすべて無視しています...

基本的に、プリミティブ型には、コンパイラーがそれらを処理する方法、およびユーザーがその型で何ができるかについて多くの特別なルールが必要ですとにかく、したがって、それらを単にとして扱うことはしばしば単純です完全に区別されます。

27
Telastyn

「基本型」を完全なオブジェクトにする必要があるのはごくわずかです(ここで、オブジェクトとは、ディスパッチメカニズムへのポインタを含むか、ディスパッチメカニズムで使用できる型でタグ付けされたデータです)。

  • ユーザー定義型を基本型から継承できるようにする必要があります。これは、パフォーマンスとセキュリティに関連する問題を引き起こすため、通常は必要ありません。コンパイルはintが特定の固定サイズを持つこと、またはメソッドがオーバーライドされていないことを想定できないためパフォーマンス上の問題であり、intsのセマンティクスが破壊される可能性があるためセキュリティ上の問題です(任意の数に等しい、または不変ではなくその値を変更する整数を検討してください)。

  • プリミティブ型にはスーパータイプがあり、プリミティブ型のスーパータイプの型を持つ変数が必要です。たとえば、intsがHashableであり、Hashableパラメータを受け取る関数を宣言して、通常のオブジェクトだけでなくintsも受け取ると仮定します。

    これは、そのような型を違法にすることで「解決」できます。サブタイプを削除して、インターフェイスが型ではなく型の制約であると判断します。明らかにそれはあなたの型システムの表現力を減らします、そしてそのような型システムはもはやオブジェクト指向と呼ばれなくなるでしょう。この戦略を使用する言語についてはHaskellを参照してください。プリミティブ型にはスーパータイプがないため、C++はその中間です。

    代替は、基本型の完全または部分的なボクシングです。ボクシングの種類は、ユーザーに表示される必要はありません。基本的には、基本型ごとに内部ボックス型を定義し、ボックス型と基本型の間の暗黙的な変換を行います。ボックス化された型のセマンティクスが異なる場合、これは扱いにくくなる可能性があります。 Javaには2つの問題があります。ボックス化された型にはアイデンティティの概念がありますが、プリミティブには値の等価性の概念しかありません。ボックス化された型はnull可能ですが、プリミティブは常に有効です。これらの問題は、提供しないことで完全に回避できます。値型のアイデンティティの概念。演算子のオーバーロードを提供し、デフォルトですべてのオブジェクトをnullにできないようにします。

  • 静的型付けは機能しません。変数は、プリミティブ型またはオブジェクトを含む任意の値を保持できます。したがって、強い型付けを保証するには、すべてのプリミティブ型を常にボックス化する必要があります。

静的型付けのある言語は、可能な場合は常にプリミティブ型を使用して、最後の手段としてボックス型にのみフォールバックします。多くのプログラムはパフォーマンスにそれほど敏感ではありませんが、プリミティブ型のサイズと構成が非常に適切である場合があります。何十億ものデータポイントをメモリに収める必要がある大規模な数値計算を考えてみてください。 doubleからfloatへの切り替えは、Cで実行可能なスペース最適化戦略である可能性がありますが、すべての数値型が常にボックス化されている場合は効果がありません(したがって、メモリの少なくとも半分が無駄になります)ディスパッチメカニズムポインタの場合)。ボックス化されたプリミティブ型をローカルで使用する場合、コンパイラ組み込み関数を使用してボックス化を削除するのはかなり簡単ですが、「十分な高度なコンパイラ」で言語の全体的なパフォーマンスを賭けるのは近視眼です。

4
amon

私が知っているほとんどの実装では、このようなクラスに3つの制限を課しているため、ほとんどの場合、コンパイラーは基本的な表現としてプリミティブ型を効率的に使用できます。これらの制限は次のとおりです。

  • 不変性
  • ファイナリティ(から派生することができない)
  • 静的型付け

Object参照が参照している場合など、コンパイラがプリミティブを基になる表現のオブジェクトにボックス化する必要の状況は比較的まれです。

これにより、コンパイラでかなり特殊なケース処理が追加されますが、神話的な超高度なコンパイラだけに限定されるわけではありません。その最適化は、主要言語の実際の製品コンパイラーにあります。 Scalaでは、独自の値クラスを定義することもできます。

2
Karl Bielefeldt

Smalltalkでは、それらすべて(int、floatなど)はファーストクラスのオブジェクトです。 onlyの特殊なケースは、SmallIntegerが効率化のために仮想マシンによって体系化され、異なる方法で処理されるため、SmallIntegerクラスはサブクラスを許可しません(これは実用的な制限ではありません)。区別はコード生成やガベージコレクションのような自動ルーチンに限定されるため、プログラマの側で特別な考慮を必要としません。

Smalltalkコンパイラ(ソースコード-> VMバイトコード)とVM nativizer(バイトコード->マシンコード))の両方が、生成されるコード(JIT)を最適化します。これらの基本オブジェクトでの基本操作のペナルティを減らすため。

1

私はOO言語とランタイムを設計していました(これは完全に異なる一連の理由で失敗しました)。

Int trueクラスのようなものを作成することに本質的に問題はありません。実際には、3(クラス、配列、およびプリミティブ)ではなく2種類のヒープヘッダー(クラスおよび配列)しかないため、GCの設計が容易になります[この後にクラスと配列をマージできるという事実は関係ありません。 ]。

プリミティブ型の本当に重要なケースは、ほとんどがfinal/sealedメソッドを持つべきです(+本当に重要です、ToStringはそれほど重要ではありません)。これにより、コンパイラーは関数自体のほとんどすべての呼び出しを静的に解決してインライン化できます。ほとんどの場合、これはコピー動作としては問題になりません(私は埋め込みを言語レベルで利用できるように選択しました(.NETもそうしました))、場合によっては、メソッドがシールされていない場合、コンパイラーはint + intの実装に使用される関数。

1
Joshua