しかし、このOOPは、パフォーマンスに基づいたソフトウェアにとって不利なものになる可能性があります。つまり、プログラムの実行速度です。
多くの場合はい!!! だが...
言い換えれば、多くの異なるオブジェクト間の多くの参照、または多くのクラスの多くのメソッドを使用すると、「重い」実装になりますか?
必ずしも。これは言語/コンパイラに依存します。たとえば、仮想関数を使用しない場合、最適化C ++コンパイラは、多くの場合、オブジェクトのオーバーヘッドをゼロに押し下げます。上のラッパーを書くようなことをすることができますint
そこにまたはこれらの単純な古いデータ型を直接使用するのと同じくらい高速に動作する単純な古いポインターにスコープ付きスマートポインターを。
Javaのような他の言語では、オブジェクトに多少のオーバーヘッドがあります(多くの場合非常に小さいことがよくありますが、非常に小さなオブジェクトでまれに天文学的です)。例えば、Integer
よりもかなり低効率があるint
(64ビット上の4とは対照的に16のバイトを要します)。しかし、これは単なる露骨な無駄やそのようなものではありません。これと引き換えに、Javaは、すべての単一のユーザー定義型に対するリフレクションのようなもの、およびとしてマークされていない関数をオーバーライドする機能を提供しますfinal
。
しかし、最良のシナリオを考えてみましょう。オブジェクトインターフェイスをゼロオーバーヘッドまで最適化できる最適化C ++コンパイラです。それでも、OOPはしばしばパフォーマンスを低下させ、ピークに達するのを防ぎます。それは完全なパラドックスのように聞こえるかもしれません。問題は次のとおりです。
インターフェイス設計とカプセル化
問題は、コンパイラがオブジェクトの構造をゼロオーバーヘッドまで押しつぶせる場合でも(少なくとも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メガから20メガに削減しました。シーケンシャルアクセスパターンでは、マシンはこのデータをかなり速く消費します。
しかし、このbirth
フィールドをもう少し詳しく見てみましょう。パーティクルが生まれる(作成される)開始時間を記録するとしましょう。パーティクルが最初に作成されたときにのみフィールドにアクセスし、画面上のランダムな場所でパーティクルが死んで生まれ変わるかどうかを10秒ごとに想像してください。その場合、birth
コールドフィールドです。パフォーマンス重視のループではアクセスされません。
その結果、パフォーマンスが重視される実際のデータは20メガバイトではなく、実際には12メガバイトの連続したブロックになります。頻繁にアクセスする実際のホットメモリは、そのサイズの半分に縮小されています!元の24メガバイトソリューションの大幅な高速化を期待してください(測定する必要はありません-すでにこの種の作業を1000回行っていますが、疑問がある場合はお気軽に)。
しかし、ここで行ったことに注意してください。このパーティクルオブジェクトのカプセル化を完全に解除しました。その状態は、Particle
型のプライベートフィールドと別の並列配列に分割されるようになりました。そして、それがきめ細かいオブジェクト指向設計の邪魔になるところです。
単一の粒子、単一のピクセル、単一の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
デザインはさよならを失いました。
これは、ユーザーが速度を要求するパフォーマンス重視の分野で働いているときに何度も起こりました。これらの小さなオブジェクト指向設計は取り壊す必要があり、カスケード破損により、より高速な設計に向けて低速の非推奨戦略を使用する必要がしばしばありました。
解決
上記のシナリオでは、オブジェクト指向のきめ細かい設計でのみ問題が発生します。そのような場合、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
潜在的に抽象的であり、仮想関数を使用することさえできます。今では意味がありません。私たちは、パーティクルごとのレベルではなく、パーティクルのコレクションレベルでオーバーヘッドを払っています。オーバーヘッドは、個別のパーティクルレベルでオブジェクトをモデリングした場合の1/100万分の1です。
したがって、これは、重い負荷を処理し、あらゆる種類のプログラミング言語(この手法はC、C ++、Python、Java、JavaScript、Lua、Swiftなどに役立つ)を処理する真のパフォーマンスクリティカルな領域のソリューションです。また、「早期最適化」というラベルを簡単に付けることはできません。これは、インターフェースの設計とアーキテクチャの関連しているため、。単一のパーティクルをオブジェクトとしてモデル化するコードベースを作成することはできません。Particle's
パブリックインターフェイスを使用してから、後で気が変わります。レガシーコードベースを最適化するために呼び出されたとき、私は多くのことをしました。これは、大きな負荷が予想される場合、事前に設計する方法に理想的に影響します。
多くのパフォーマンスに関する質問、特にオブジェクト指向の設計に関する質問では、この答えを何らかの形で繰り返します。オブジェクト指向の設計は、最高のパフォーマンス要件を満たすことができますが、それに対する考え方を少し変更する必要があります。そのチーターにできるだけ速く走らせるための余地を与えなければなりません。また、状態をほとんど格納しない小さなオブジェクトを設計する場合、それはしばしば不可能です。