オブジェクト指向は、多くのアルゴリズムを実装するのに非常に役立ちました。ただし、オブジェクト指向言語は「単純明快」なアプローチでガイドすることがあります。このアプローチが常に良いことであるかどうかは疑問です。
OOは、アルゴリズムを高速かつ簡単にコーディングするのに非常に役立ちます。しかし、これはOOPは、パフォーマンスに基づいたソフトウェアにとって不利になる可能性があります。つまり、プログラムの実行速度はどうでしょうか?
たとえば、グラフノードをデータ構造に格納することは、最初は「簡単」のように見えますが、Nodeオブジェクトに多くの属性とメソッドが含まれている場合、アルゴリズムが遅くなる可能性がありますか?
言い換えると、多くの異なるオブジェクト間の多くの参照、または多くのクラスの多くのメソッドを使用すると、「重い」実装になる可能性がありますか?
オブジェクト指向は、カプセル化のために、特定のアルゴリズムの最適化を妨げる場合があります。 2つのアルゴリズムは特にうまく連携しますが、OOインターフェースの背後に隠されている場合、それらの相乗効果を使用する可能性は失われます。
数値ライブラリを見てください。それらの多く(60年代や70年代に書かれたものだけでなく)はOOPではありません。それには理由があります-数値アルゴリズムは、OO階層とインターフェースおよびカプセル化よりも、分離されたmodules
のセットとしてより効果的に機能します。
基本:データ構造、アルゴリズム、コンピュータアーキテクチャ、ハードウェア。さらにオーバーヘッド。
OOPプログラムは、 正確に合わせる CS理論で最適と見なされるデータ構造とアルゴリズムの選択。最適なプログラムと同じパフォーマンス特性に加えて、いくらかのオーバーヘッドがあります。通常、オーバーヘッドを最小限に抑えることができます。
ただし、 最初に OOPの懸念事項のみで設計され、ファンダメンタルズは考慮されないため、最初は準最適である場合があります。準最適性は、リファクタリングによって削除できる場合があります。完全に書き直す必要がない場合もあります。
はい。ただし、桁違いに市場投入までの時間(TTM)の方が重要です。ビジネスソフトウェアは、複雑なビジネスルールへのコードの適応性に重点を置いています。パフォーマンス測定は、開発ライフサイクル全体を通じて行う必要があります。 (セクションを参照: 最適なパフォーマンスとはどういう意味ですか?)市場性のある機能強化のみを行う必要があり、後のバージョンで徐々に導入する必要があります。
一般に、ソフトウェアパフォーマンスの問題は、「より速いバージョンが存在する」ことを証明するために、そのより速いバージョンが最初に存在する必要があることです(つまり、それ以外の証拠はありません)。
時には、その高速バージョンが最初に別の言語またはパラダイムで見られます。これは改善へのヒントであり、他の言語やパラダイムの劣等性の判断ではありません。
OOPは、「作業性」を改善する見返りとして、コードのビジネス価値を高める代わりに、オーバーヘッド(スペースと実行)をもたらします。これにより、さらなる開発と最適化のコストが削減されます。 @ MikeNakis を参照してください。
OOPの部分は、(i)シンプルさ/直感性を促進する、(ii)ファンダメンタルズの代わりに口語的な設計方法を使用する、(iii)同じ目的の複数の調整された実装を阻止する.
一部のOOP=ガイドライン(カプセル化、メッセージパッシング、1つのことを上手く行う)を厳密に適用すると、実際には最初はコードが遅くなります。パフォーマンス測定はこれらの問題の診断に役立ちます。データ構造とアルゴリズムは、理論で予測された最適設計と一致し、オーバーヘッドを通常最小限に抑えることができます。
前述のように、設計に最適なデータ構造を使用します。
一部の言語は、一部のランタイムパフォーマンスを回復できるコードのインライン展開をサポートしています。
OOPと基礎を学びます。
OOPを厳密に遵守すると、高速バージョンを作成できなくなる可能性があります。高速バージョンを最初から作成することしかできない場合もあります。そのため、異なるバージョンを使用して複数バージョンのコードを作成すると便利ですアルゴリズムとパラダイム(OOP、汎用、関数、数学、スパゲッティ)を使用し、最適化ツールを使用して、各バージョンを観察された最大パフォーマンスに近づけます。
([@quant_dev]、[@ SK-logic]、[@ MikeNakis]の間の議論から拡大)
それは、コンテナのようにオブジェクト指向のことではありません。ダブルリンクリストを使用してピクセルをビデオプレーヤーに格納すると、問題が発生します。
ただし、正しいコンテナを使用する場合、標準よりもstd :: vectorの方が遅い理由はありません。また、エキスパートによる一般的なアルゴリズムがすべて既に記述されているため、ホームロールされた配列コードよりも高速です。
OOPは明らかに優れたアイデアであり、他の優れたアイデアと同様に、使いすぎることがあります。私の経験では、それはかなり使いすぎです。パフォーマンスが低下し、保守性が低下します。
これは、仮想関数の呼び出しのオーバーヘッドとは関係がなく、オプティマイザー/ジッターが行うこととは関係ありません。
これは、最高のbig-Oパフォーマンスを持ちながら、非常に悪い一定の要因を持つデータ構造に関係しています。これは、アプリにパフォーマンス制限の問題がある場合、それは別の場所にあるという前提で行われます。
これが現れる1つの方法は、1秒あたりの回数newが実行されることです。これは、O(1) =パフォーマンス。ただし、数百から数千の命令を実行できます(一致するdeleteまたはGC時間を含む)。使用済みオブジェクトを保存することで軽減できますが、コードの「クリーンさ」を低下させます。
別の方法としては、プロパティ関数、通知ハンドラ、基本クラス関数の呼び出し、一貫性を維持するために存在するあらゆる種類の地下関数呼び出しの記述が推奨されます。一貫性を維持するために、それらは限られた成功を収めていますが、サイクルを無駄にすることで大成功しています。プログラマーは正規化されたデータの概念を理解していますが、データベース設計にのみ適用する傾向があります。少なくとも一部はOOPは必要ないことを示しているため、データ構造設計には適用されません。オブジェクトにModifiedビットを設定するだけで、津波が発生する可能性があります。コードの価値のあるクラスがModified呼び出しを取り、storesするだけなので、データ構造を通じて実行される更新の数。
おそらく、特定のアプリのパフォーマンスは、記述されているとおりに問題ないでしょう。
一方、パフォーマンスの問題がある場合は、- これがチューニングの方法の例です 。これは多段階のプロセスです。各段階で、特定のアクティビティは時間の大部分を占め、より速いものに置き換えることができます。 (私は「ボトルネック」とは言いませんでした。これらはプロファイラーが見つけるのが得意な種類のものではありません。)このプロセスでは、高速化のために、データ構造の大規模な置き換えが必要になることがよくあります。多くの場合、このデータ構造が存在するのは、推奨されるOOPプラクティスのためです。
理論的には、低速になる可能性がありますが、それでも低速なアルゴリズムではなく、低速な実装になります。実際には、オブジェクト指向を使用すると、さまざまなwhat-ifシナリオを試す(または将来アルゴリズムを再検討する)ことができるため、algorithmicの改善が提供されます。そもそもスパゲッティの方法です。なぜなら、その作業は困難なものになるからです。 (基本的に、全体を書き直す必要があります。)
たとえば、さまざまなタスクとエンティティを分割してオブジェクトをクリーンカットすることで、後で簡単にアクセスできるようになり、たとえば、いくつかのオブジェクト(それらに対して透過的)の間にキャッシュ機能を埋め込むことができます。倍の改善。
一般に、低水準言語(または高水準言語での巧妙なトリック)を使用して達成できる改善のタイプは、一定の(線形)時間改善をもたらしますが、big-oh表記では計算できません。アルゴリズムの改善により、非線形の改善を実現できる場合があります。それは貴重です。
しかし、これはOOPは、パフォーマンスに基づいたソフトウェアにとって不利になる可能性があります。つまり、プログラムの実行速度はどうでしょうか?
言い換えれば、多くの異なるオブジェクト間の多くの参照、または多くのクラスの多くのメソッドを使用すると、「重い」実装になる可能性がありますか?
必ずしも。これは言語/コンパイラに依存します。たとえば、仮想関数を使用しない場合、最適化C++コンパイラーは、多くの場合、オブジェクトのオーバーヘッドをゼロに押しつぶします。そこにint
を介してラッパーを記述したり、これらのプレーンな古いデータ型を直接使用するのと同じくらい高速に実行されるプレーンな古いポインターを介したスコープ付きスマートポインターを記述したりできます。
Javaのような他の言語では、オブジェクトへのオーバーヘッドが少しあります(多くの場合、非常に小さい場合が多いですが、非常に小さいオブジェクトの場合はまれに天文学的です)。たとえば、Integer
はint
よりも効率がかなり低くなります(64ビットでは4バイトであるのに対し、16バイトが必要です)。しかし、これは単なる露骨な廃棄物やそのようなものではありません。代わりに、Javaは、すべてのユーザー定義型を一律に反映したり、final
としてマークされていない関数をオーバーライドしたりする機能などを提供します。
それでもベストケースのシナリオを見てみましょう。オブジェクトインターフェイスをzeroオーバーヘッドまで最適化できる最適化C++コンパイラです。それでも、OOPはパフォーマンスを低下させ、ピークに達するのを防ぎます。それは完全なパラドックスのように聞こえるかもしれません。どうすればよいでしょうか?問題は次のとおりです:
問題は、コンパイラーがオブジェクトの構造をzeroオーバーヘッド(少なくともC++コンパイラーの最適化では少なくとも非常に頻繁に当てはまる)に押しつぶすことができる場合でも、カプセル化とインターフェース設計(および依存関係の蓄積)が細かいことです。粒子の粗いオブジェクトは、質量によって集約されることを目的としたオブジェクトの最適なデータ表現を妨げることがよくあります(これは、多くの場合、パフォーマンスが重要なソフトウェアの場合です)。
この例を見てみましょう:
class Particle
{
public:
...
private:
double birth; // 8 bytes
float x; // 4 bytes
float y; // 4 bytes
float z; // 4 bytes
/*padding*/ // 4 bytes of padding
};
Particle particles[1000000]; // 1mil particles (~24 megs)
私たちのメモリアクセスパターンは、単純にこれらのパーティクルを順番にループして、各フレームの周りで繰り返し移動し、画面の隅から跳ね返して結果をレンダリングすることだとしましょう。
すでにパーティクルが連続して集約されている場合、birth
メンバーを適切に配置するために必要な目立った4バイトのパディングオーバーヘッドがすでにわかります。メモリーの約16.7%は、アライメントに使用されるデッドスペースで無駄になっています。
最近はギガバイトのDRAMがあるので、これは意味がないように思えるかもしれません。それでも、私たちが今日持っている最も凶暴なマシンでさえ、CPUキャッシュ(L3)の低速で最大領域に関しては、わずか8メガバイトしかないことがよくあります。そこに収まらないほど、DRAMへの繰り返しアクセスという点で料金が高くなり、処理が遅くなります。突然、メモリの16.7%を浪費することは、もはや簡単なことではなくなったように見えます。
フィールドの配置に影響を与えることなく、このオーバーヘッドを簡単に排除できます。
class Particle
{
public:
...
private:
float x; // 4 bytes
float y; // 4 bytes
float z; // 4 bytes
};
Particle particles[1000000]; // 1mil particles (~12 megs)
double particle_birth[1000000]; // 1mil particle births (~8 bytes)
これで、メモリが24 MBから20 MBに減りました。シーケンシャルアクセスパターンを使用すると、マシンはこのデータをかなり速く消費するようになります。
しかし、このbirth
フィールドをもう少し詳しく見てみましょう。パーティクルが生成(作成)された開始時間を記録するとします。パーティクルが最初に作成されたときにのみフィールドにアクセスし、10秒ごとにパーティクルが死んで画面上のランダムな場所で生まれ変わるかどうかを想像してください。その場合、birth
はコールドフィールドです。パフォーマンスが重要なループではアクセスされません。
その結果、実際のパフォーマンスが重要なデータは20メガバイトではなく、実際には12メガバイトの連続したブロックになります。私たちが頻繁にアクセスしている実際のホットメモリはhalfに縮小されています!期待する重要元の24メガバイトのソリューションよりも高速化(測定する必要はありません-この種のことはすでに1000回行われていますが、疑わしい場合は遠慮しないでください)。
ここで私たちが何をしたかに注意してください。このパーティクルオブジェクトのカプセル化を完全に解除しました。その状態はParticle
タイプのプライベートフィールドと個別の並列配列に分割されています。そして、それがgranularオブジェクト指向設計が邪魔になるところです。
単一の粒子、単一のピクセル、単一の4成分ベクトル、ゲーム内の単一の「生き物」オブジェクトなど、単一の非常に細かいオブジェクトのインターフェース設計に限定した場合、最適なデータ表現を表現できません。などチーターが2平方メートルの小さな島に立っている場合、チーターの速度は無駄になります。これは、パフォーマンスの点で非常に細かいオブジェクト指向設計がよく行うことです。データ表現を次善の性質に制限します。
これをさらに進めるために、粒子を移動しているだけなので、実際には3つの別々のループでx/y/zフィールドにアクセスできるとしましょう。その場合、8つのSPFP演算を並列にベクトル化できるAVXレジスタを備えたSoAスタイルのSIMD組み込み関数を利用できます。しかし、これを行うには、次の表現を使用する必要があります。
float particle_x[1000000]; // 1mil particle X positions (~4 megs)
float particle_y[1000000]; // 1mil particle Y positions (~4 megs)
float particle_z[1000000]; // 1mil particle Z positions (~4 megs)
double particle_birth[1000000]; // 1mil particle births (~8 bytes)
さて、粒子シミュレーションで飛行していますが、粒子設計に何が起こったのか見てみましょう。それは完全に取り壊されており、現在4つの並列配列を調べており、それらを集約するオブジェクトはありません。私たちのオブジェクト指向のParticle
デザインは、さよならしました。
これは、ユーザーがスピードを要求し、正確性だけが要求するパフォーマンス重視の分野で何度も働いていました。これらの小さな10代のオブジェクト指向設計は解体する必要があり、連鎖的な破損により、より高速な設計に向けてゆっくりと非推奨にする戦略を使用することがしばしば要求されました。
上記のシナリオは、granularオブジェクト指向の設計に関する問題のみを示しています。これらの場合、SoA担当者、ホット/コールドフィールド分割、シーケンシャルアクセスパターンのパディング削減の結果としてより効率的な表現を表現するために、構造を解体しなければならないことがよくあります(パディングは、ランダムアクセスのパフォーマンスに役立つ場合があります) AoSケースのパターンですが、ほとんどの場合、シーケンシャルアクセスパターンの妨げになります)など。
それでも、私たちが決着した最終的な表現を取り、オブジェクト指向のインターフェースをモデル化することができます。
// Represents a collection of particles.
class ParticleSystem
{
public:
...
private:
double particle_birth[1000000]; // 1mil particle births (~8 bytes)
float particle_x[1000000]; // 1mil particle X positions (~4 megs)
float particle_y[1000000]; // 1mil particle Y positions (~4 megs)
float particle_z[1000000]; // 1mil particle Z positions (~4 megs)
};
今、私たちは元気です。私たちは好きなオブジェクト指向のすべてのものを手に入れることができます。チーターには、国全体をできるだけ早く走らせることができます。私たちのインターフェース設計はもはや私たちをボトルネックの隅に閉じ込めません。
ParticleSystem
は、抽象的で仮想関数を使用する可能性さえあります。頭が痛いので、per-particleレベルではなく、particle of collectionsレベルでオーバーヘッドを支払います。オーバーヘッドは、個々のパーティクルレベルでオブジェクトをモデリングした場合と比べて、1/1,000,000です。
だから、それは重い負荷を処理する真のパフォーマンスクリティカル領域でのソリューションであり、all種類のプログラミング言語(この手法はC、C++、Python、Java、JavaScript、Lua、Swiftなどにメリットがあります) 。また、これはインターフェース設計およびアーキテクチャに関連しているため、「時期尚早の最適化」と簡単にラベル付けすることはできません。単一のパーティクルをモデル化するコードベースを、Particle's
パブリックインターフェイスへのクライアント依存関係のボートロードを持つオブジェクトとして記述して、後で考え方を変えることはできません。レガシーコードベースを最適化するために呼び出されたとき、私は多くのことを行いました。その結果、より大きなデザインを使用するために数万行のコードを慎重に書き換えるのに何ヶ月もかかる可能性があります。これは理想的には、重い負荷を予測できる場合、事前の設計方法に影響します。
私は多くのパフォーマンスの質問、特にオブジェクト指向設計に関連する質問で、この答えを何らかの形で繰り返します。オブジェクト指向の設計は、依然として最も要求の高いパフォーマンスのニーズと互換性がありますが、それについての考え方を少し変更する必要があります。私たちはそのチーターに可能な限り高速で実行する余地を与える必要があり、ほとんど状態を保存しない小さなオブジェクトを設計する場合、それはしばしば不可能です。
はい、オブジェクト指向の考え方は、アルゴリズムレベルと実装レベルの両方で、高性能プログラミングに関しては中立的または否定的です。 OOPがアルゴリズム分析に取って代わる場合、これは時期尚早の実装につながる可能性があり、最低レベルではOOP抽象化は脇に置く必要があります。
この問題は、OOPが個々のインスタンスについて考えることに重点を置いていることに起因します。 OOP=アルゴリズムについての考え方は、特定の値のセットについて考え、それをそのように実装することによると言えます。それがあなたの最高レベルのパスであれば、 Big Oの獲得につながるような変革や再構築を実現することはほとんどありません。
アルゴリズムのレベルでは、より大きな全体像と、Big Oの増加につながる制約と値の間の関係について考えることがよくあります。たとえば、OOPマインドセットには、「整数の連続範囲の合計」をループから(max + min) * n/2
実装レベルでは、コンピュータはほとんどのアプリケーションレベルのアルゴリズムに対して「十分に高速」ですが、低レベルのパフォーマンスが重要なコードでは、局所性について非常に心配しています。繰り返しますが、OOP個々のインスタンスとループを通過する1つの値について考えることに重点を置くと、負の値になる可能性があります。高性能のコードでは、単純なループを作成する代わりに、ループを部分的に展開するには、いくつかのロード命令を上部にグループ化し、それらをグループに変換してから、グループに書き込みます。中間の計算、そしてキャッシュとメモリアクセスに非常に注意を払っている間、 ; OOP抽象化が無効になっている問題。そして、従うと誤解を招く可能性があります。このレベルでは、マシンレベルの表現について知って考えなければなりません。
インテルのパフォーマンスプリミティブのようなものを見ると、文字通り数千の高速フーリエ変換の実装があり、それぞれが特定のデータに対してよりうまく機能するように調整されています-サイズとマシンアーキテクチャ。 (興味深いことに、これらの実装の大部分はマシン生成であることがわかります: MarkusPüschel自動パフォーマンスプログラミング )
もちろん、ほとんどの答えが言っているように、ほとんどの開発、ほとんどのアルゴリズムでは、OOPはパフォーマンスに関係ありません。「時期尚早に悲観化」し、多くを追加しない限り、非ローカル呼び出しの場合、this
ポインタはここにもそこにもありません。
その関連、そしてしばしば見落とされています。
簡単な答えではありませんそれはあなたが何をしたいかによって異なります。
一部のアルゴリズムは、プレーンな構造化プログラミングを使用した方がパフォーマンスが優れていますが、他のアルゴリズムはオブジェクト指向を使用した方が優れています。
オブジェクト指向の前は、多くの学校が構造化プログラミングを使用してアルゴリズム設計を教えています。今日、多くの学校が、アルゴリズム設計とパフォーマンスを無視して、オブジェクト指向プログラミングを教えています。
もちろん、そこには構造化プログラミングを教える学校があり、アルゴリズムにはまったく関心がありませんでした。
パフォーマンスはすべて、最終的にはCPUとメモリサイクルにまで及びます。ただし、OOPメッセージングとカプセル化のオーバーヘッドと、より広いオープンプログラミングセマンティクスとの間の割合の違いは、アプリケーションのパフォーマンスに顕著な違いをもたらすのに十分な割合である場合とそうでない場合があります。アプリの場合ディスクまたはデータキャッシュミスの限界がある場合、OOP=オーバーヘッドがノイズで完全に失われる可能性があります。
しかし、リアルタイム信号と画像処理の内部ループ、およびその他の数値計算バインドアプリケーションでは、CPUとメモリのサイクルのかなりの割合で違いが生じる可能性があり、あらゆるOOPオーバーヘッドが発生する可能性があります。実行するにははるかにコストがかかります。
特定のOOP言語のセマンティクスは、コンパイラがこれらのサイクルを最適化するための十分な機会を公開する場合としない場合があります。または、CPUの分岐予測回路が常に正しく推測してこれらのサイクルを事前にカバーする場合があります。フェッチとパイプライン処理。
優れたオブジェクト指向設計は、アプリケーションを大幅に高速化するのに役立ちました。 Aはアルゴリズム的な方法で複雑なグラフィックを生成する必要がありました。私はMicrosoft Visioの自動化を通じてそれを行いました。私は働きましたが、信じられないほど遅かったです。幸いなことに、私はロジック(アルゴリズム)とVisioのものの間に追加の抽象化レベルを挿入しました。私のVisioコンポーネントは、インターフェイスを介してその機能を公開しました。これにより、遅いコンポーネントを別のSVGファイルに簡単に置き換えることができました。これは、少なくとも50倍高速でした。クリーンなオブジェクト指向のアプローチがなければ、アルゴリズムとVisionコントロールのコードが絡み合ってしまい、変更が悪夢になっていたでしょう。