web-dev-qa-db-ja.com

プロトタイプの継承は本質的に遅いですか?

Javascript 6は従来のクラスベースの継承を追加することがわかります。また、ネイティブコードまたはバイトコードへの(JIT)コンパイルを行うときにコンパイラーによって最適化できるため、クラスは本質的にプロトタイプよりもはるかに高速であると聞いています。この種のことは私の心には理にかなっていますが、私はこのことを本当に理解するほどの高度なプログラマではありません。

私がプロトタイプ継承(PI)についていつも見た議論は、それがメモリを節約するということでした。これは、最近ではほとんど完全に議論の余地があるポイントです。個人的に、私はPIが好きで、常に動的言語で「理にかなっている」と感じていました。それは、インタプリタ型動的言語のパフォーマンスの制限に既に妥協しているのであれば、それは贅沢であるように、十分に余裕があります。

C++では、すべてがパフォーマンスの可能性を最大限に引き出すというC++の哲学に伴う制限のため、クラスは理にかなっています。

もちろん、C++でPIを使用でき、そのためのよく知られた設計パターンがありますが、私は動的言語の組み込み継承について話しています。

架空の動的言語でのプロトタイプ継承の可能な限り最速の実装を見ていた場合、従来のクラスと比較してパフォーマンスに関する欠点は何でしょうか?

一部のオブジェクトが「最終」であり、そのプロトタイプチェーンを実行時に変更できないように指定できる機能を架空の言語に追加するとどうなりますか?(従来のPIと共存)オプションの静的型付けまたは型ヒントも追加しますこれはプロトタイプ言語に役立ちます。

これは少し漠然とした質問と見なされる可能性があるため、以下を追加します。

私よりも、言語理論やコンパイラ/インタプリタの記述に、より賢く、より興味がある人々から、この主題についての考えを探していると思います。それがまだ大丈夫な質問であることを願っています。

これはJavascript 6についてのことではありません。Javascriptが最も成功したプロトタイプ言語であり、Javascriptがネイティブクラスを追加しているのは興味深いことです。 -過去数年間のベンチマークの改善の低下)。また、Javascriptが遅いと信じている、または思っていると言っているわけではありません。

本当に、私はプログラミング言語についてもっと学びたいと思っています。おそらく、いつか自分の動的言語を構築することを目標としています。

7

理解すべきことが2つあります。

  1. プロトタイプの継承は、パフォーマンスとは関係ありませんまったく。パフォーマンスの問題は、継承構造のruntimeの変更に起因します。
  2. プロトタイプの継承(および柔軟なオブジェクト構造)は、最適化が難しいため、本質的にそれほど遅くはありません。

最初のクレームを説明するために、Rubyは、クラスベースのオブジェクトがabysmallyの低速であることのフラグシップの例です。問題は、メソッドにメッセージパッシングを使用するSmalliveのような言語(Objective-Cなど)全体で持続します。 Objective-Cランタイムは、問題に取り組むために気の利いたメソッドキャッシュを使用しますが、それを実現するのに時間がかかりました。

メソッド呼び出しを安くするのは、コンパイラー(またはJITやランタイムさえも)がオブジェクトの構造について明確な知識を持っているということです。言語機能に関して明示的に、または静的分析から暗黙的に推論されたとしても、知識は存在し、コンパイラーはそれを使用して最適化できます。

ここで、実行時に構造が変化する可能性がある場合、これは少しトリッキーになります。コードのどの部分を最適化する価値があるかを検出するための優れたヒューリスティックが必要です(実行されるコードの頻度とそれが変化する頻度とそれを最適化するためのコスト)。


それでは、動的言語のクラスのポイントは何ですか? (Extのような)JavaScriptクラスシステムは多数あるので、そうでなければなりません。もちろん、クラスに慣れているプログラマーにとって親しみやすさは1つの理由です。しかし、本当の利点は、オブジェクトタイプのexplicit定義を確実に支援することにあります。このようなクラス定義はone(複雑ですが)ステートメントです。バニラJavaScriptコンストラクターを使用すると、運が良ければ、たくさんのステートメントがグループ化されます。クラス構造は、実際には、命令型コードの副作用にすぎません。クラス宣言は当然ながら、より宣言的なスタイルを促進することを目的としています。

プロトタイプの継承を明示的に明示したSelf言語(メッセージパッシングを使用する言語で実現するのは非常に簡単ですが)は、完全にインタラクティブなシステムをプログラミングした環境用に作成されたものですwhile走っていた。実際に画面上にオブジェクトを見ることができます。これにより、宣言と結果が密接に結び付けられたため、実行時に変更可能なクリーンな宣言が可能になりました。そのような結合がないと、実行時にオブジェクト構造をいじると、理由を言うまでもなく、脳に収まるのが非常に難しいわかりにくい混乱になる可能性があります。


委任に対する十分なサポートがあれば、プロトタイプの継承をほとんど得ることができます。プロトタイプと見なすオブジェクトへのすべての実装されていない呼び出しを委任すれば完了です。より柔軟です。ただし、最適化も同様に困難です。

4
back2dos

C++では、すべてがパフォーマンスの可能性を最大限に引き出すというC++の哲学に伴う制限のため、クラスは理にかなっています。

このステートメントには、2つの誤った前提があります。 1つ目は、クラスはC++のものであることです。そうではありません。概念はOOP自体と同じで、最初のオブジェクト指向言語であるSimulaにまでさかのぼります。それ以来、ほぼすべてのOO言語はクラスを持っています。 Pythonなどの動的なモデルを含みます。これは、異常なプロトタイプモデルです。

2つ目は、パフォーマンスのためにクラスが使用されることです。これは確かに1つの利点ですが、使用する理由はほとんどありませんthe。クラスを使用する本当の理由は、それらが多くの重要なタイプのプログラミング問題をモデル化する自然な方法であり、正確性を強制し、問題のクラス全体を回避する方法として静的型付けと非常にうまく一致することです(しゃれは意図されていません) )コンパイル時。

そしてはい、クラスは実行時のプロトタイプ継承よりも本質的に高速です。1つの単純な理由があります。コンパイル時に計算できる情報は、実行時に計算する必要はありません。クラスは、コンパイラに事前に違反がないことを知ることができる特定の不変条件を提供します。これにより、それらをチェックする必要のない、より単純なランタイムコードを作成できます。

クラスの実装方法に応じて、2番目の大きな利点があります。オブジェクトがフィールド付きの連続したレコードとして実装されている場合、メンバーアクセスは、オブジェクトのメモリ位置の先頭から一定のオフセットにあるポインターの逆参照になります。オブジェクトのレイアウトがコンパイラに知られていない場合、実行時にハッシュテーブルまたは他のマップでメンバーを検索するよりも数十倍、さらには数百倍高速になります。

つまり、クラスは本質的に高速です。そのため、最新のJavaScript JITは、可能な限りオブジェクトをクラスに変換しようとします。しかし、これは単なる副次的な利点です。実際の利点は、それらが本質的にsaferであり、コードの動作について推論および証明するためのツールとしてより有用であることです。

1
Mason Wheeler

私の知る限り、2つの間に大きな違いはありません。 C++クラスは実際には、最初にvtableと呼ばれる特別な非表示のデータ構造を持つデータのブロックであると考えてください。このvtableには、クラスに含まれるメソッドへのすべてのポインタが含まれています。

これで、JavaScriptクラスのプロトタイプ(jsクラスを構成する関数へのポインターの集まり)と大差ないことがわかります。

ただし、実際にはその違いは非常に大きくなります。 C++では、これらの関数ポインターを動的に追加、削除、または置換できないように制限しています。これは、コンパイラが事前にそれらが何であるかを知っていることを意味し、完全に最適化することができます。また、関数がどこにあるかを正確に把握し、価値があると考えられる場合はメソッドをインライン化することもできます(たとえば、多くのプロパティメソッドでは、getID(){return id;}。これは完全に理にかなっています)。

したがって、C++バイナリでは、プログラム命令の単純な線形ステップであるコードを取得することがよくあります。 CPUはコードのチャンクをキャッシュにロードし、可能な限り高速に実行します。これが、最近のCPUが実行するものです。

CPUが実行を停止するのは、他の場所からコードのチャンクを停止してロードする必要がある場合です。したがって、コンパイル時に関数が不明であり、どこにでもある可能性があるJavaScriptプログラムの場合、コンパイルされたプログラムコードは、メソッドの場所に命令をジャンプする代わりに実行されます。運が悪い場合は、CPUが処理を停止し、メソッドを含む新しいコードブロックをロードして実行し、元の場所に戻る必要があることを意味します。これは、単純な例で単に「id」を読み取るよりも効率が悪いことがわかります。

これはC++のものではないことに注意してください。非常に初期のC +コンパイラはCへのトランスレータとして作成されていたため、C++クラスは、Cコンパイラによってコンパイルされる構造体と関数ポインタのセットを持つCコードに変換されました。

私は多くの複雑化(一部のC++ポリモーフィズムを含む)と最適化および「回避策」のCPUとコンパイラーがこれをよりよく機能させるために実行できることについて説明してきましたが、その背後にある基本的な考え方を理解しています。

高速言語を設計する場合は、コンパイル段階で多くの情報を計算できるようにして、実行時に実行する必要がないようにします。プロトタイプベースのクラスシステムを設計し、プログラム全体を解析して、各関数がどこにあるのかを事前に把握できれば、C++プログラムと同様に機能するマシンコードにコンパイルできます。問題は、そのような解析は実行するよりも複雑になる可能性があることです。 (一部のC++コンパイラには ランタイム最適化メカニズム があり、ランタイム情報が計算され、後続のコンパイルにフィードバックするために使用されることに注意してください!)

0
gbjbaanb