「いい」オブジェクト指向のコードを書くことと、非常に高速で低レイテンシーのコードを書くことの間にトレードオフがあると思いますか?たとえば、C ++の仮想関数/多態性などのオーバーヘッドを回避する-見た目は悪いが非常に高速なコードを書き換えるなど
私はレイテンシーよりもスループットに少し焦点を当てた分野で働いていますが、それは非常にパフォーマンスが重要であり、「sorta」と言います。
しかし、問題は、非常に多くの人々がパフォーマンスの概念を完全に間違っていることです。初心者は多くの場合、すべてが間違っているだけであり、「計算コスト」の概念モデル全体を修正する必要があります。アルゴリズムの複雑さだけが正しいものです。中間体は多くのことを間違えます。専門家はいくつかの点を間違えています。
キャッシュミスや分岐の予測ミスなどのメトリックを提供できる正確なツールで測定することは、この分野のあらゆるレベルの専門知識を持つすべての人々を抑制し続けるものです。
また、測定は、最適化しないものを指し示します。専門家は、真の測定されたホットスポットを最適化し、遅くなる可能性があるという予測に基づいて暗闇の中で野生の刺し傷を最適化しようとしないため、初心者よりも最適化にかかる時間が少なくなります(極端な形では、コードベースの他のすべての行について)。
パフォーマンスのための設計
それはともかく、パフォーマンスのための設計の鍵は、インターフェース設計のように、設計部分にあります。経験不足の問題の1つは、一般的なコンテキストでの間接的な関数呼び出しのコストなど、絶対的な実装メトリックに早期のシフトが生じる傾向があることです。これは、コスト(オプティマイザーの点からすぐに理解した方がよい分岐点ではなくビューの方向)は、コードベース全体で回避する理由です。
コストは相対的です。間接的な関数呼び出しにはコストがかかりますが、たとえば、すべてのコストは相対的です。数百万の要素をループする関数を呼び出すためにそのコストを一度支払う場合、このコストを心配することは、数十億ドルの製品を購入するために何時間も費やして、その製品を購入しないと結論づけるようなものです1ペニー高すぎました。
より粗いインターフェース設計
パフォーマンスのインターフェース設計の側面は、これらのコストをより粗いレベルに押し上げるために、以前より早く追求することがよくあります。たとえば、単一のパーティクルの実行時抽象化コストを支払う代わりに、そのコストをパーティクルシステム/エミッタのレベルに押し上げ、パーティクルを実装の詳細および/またはこのパーティクルコレクションの単純な生データに効果的にレンダリングします。
そのため、オブジェクト指向の設計はパフォーマンスの設計(レイテンシまたはスループット)と互換性がある必要はありませんが、それを重視する言語では、ますます小さな粒状オブジェクトをモデル化する誘惑がある場合があり、最新のオプティマイザーはできません助けて。ソフトウェアのメモリアクセスパターンの効率的なSoA表現を生成する方法で、単一のポイントを表すクラスを結合するようなことはできません。粗さのレベルでモデル化されたインターフェース設計を持つポイントのコレクションは、その機会を提供し、必要に応じてより最適なソリューションに向けて反復することを可能にします。このような設計は、大容量メモリ用に設計されています*。
* ここでは、データではなくメモリに重点を置きます。パフォーマンスが重要な領域で長時間作業すると、データ型とデータ構造のビューが変更され、メモリへの接続方法が表示されるためです。バイナリ検索ツリーは、固定アロケーターの助けを借りない限り、ツリーノードのキャッシュフレンドリとは異なる可能性のあるメモリチャンクなどの場合に、対数の複雑さだけになりません。このビューはアルゴリズムの複雑さを排除しませんが、メモリレイアウトとは独立して表示されなくなります。また、作業の反復は、メモリアクセスの反復に関するものであると見なされ始めます。*
多くのパフォーマンス重視の設計は、実際には、人間が理解して使用しやすい高レベルのインターフェース設計の概念と非常に互換性があります。違いは、このコンテキストでの「高レベル」は、メモリのバルク集約、潜在的に大規模なデータコレクション用にモデル化されたインターフェイス、および非常に低レベルである可能性のある実装に関することです。視覚的なアナロジーは、非常に快適で運転や操作が簡単で、音の速さで非常に安全な車かもしれませんが、ボンネットを開けると、火を吐く悪魔はほとんどいません。
より粗い設計では、より効率的なロックパターンを提供し、コードの並列性を活用するための簡単な方法も得られる傾向があります(マルチスレッドは、ここではスキップしますので、網羅的なテーマです)。
メモリプール
低レイテンシプログラミングの重要な側面は、参照の局所性とメモリの割り当てと割り当て解除の一般的な速度を向上させるために、メモリを非常に明示的に制御することです。カスタムアロケータープーリングメモリは、実際には、説明したのと同じ種類の設計思想を反映しています。バルク用に設計されています。粗いレベルで設計されています。大きなブロックでメモリを事前に割り当て、小さなチャンクですでに割り当てられているメモリをプールします。
アイデアは、高価なものをプッシュすることとまったく同じです(たとえば、汎用のアロケーターに対してメモリチャンクを割り当てる)。メモリプールは、メモリを一括処理するために設計されています。
タイプシステム分離メモリ
任意の言語での粒度の高いオブジェクト指向設計の難しさの1つは、多くの小さなユーザー定義型とデータ構造を導入したいということです。これらのタイプは、動的に割り当てられた場合、小さな塊で割り当てられます。
C ++の一般的な例は、ポリモーフィズムが必要な場合で、自然な誘惑はサブクラスの各インスタンスを汎用メモリアロケーターに割り当てることです。
これにより、連続する可能性のあるメモリレイアウトがアドレス範囲全体に散らばる小さなビット単位のビットと断片に分割され、ページフォールトとキャッシュミスが増加します。
低遅延でスタッターのない決定論的応答を必要とする分野は、ホットスポットが常に単一のボトルネックに沸騰するわけではない場所であり、小さな非効率性が実際に「蓄積」(多くの人が想像するもの)することができますプロファイラーでそれらをチェックしておくのは間違っていますが、遅延駆動型のフィールドでは、実際には小さな非効率性が蓄積するまれなケースがあります)。そして、そのような蓄積の最も一般的な理由の多くは、これである可能性があります:いたるところにメモリの小さなチャンクの過剰な割り当て。
Javaのような言語では、たとえば、int
(しかしまだかさばる高レベルインターフェイスの背後にある)配列などのボトルネックのある領域(タイトループで処理される領域)に対して、可能な限り単純な古いデータ型の配列を使用すると便利です、ArrayList
ユーザー定義のInteger
オブジェクト。これにより、通常、後者に伴うメモリ分離が回避されます。C ++では、メモリ割り当てパターンが効率的であれば、ユーザー定義型を連続的に割り当てることができ、汎用コンテナのコンテキストでさえ割り当てることができるため、構造をそれほど劣化させる必要はありません。
メモリーの融合
ここでの解決策は、同種のデータ型、場合によっては同種のデータ型のカスタムアロケータに到達することです。小さなデータ型とデータ構造がメモリ内のビットとバイトにフラット化されると、それらは同種の性質を帯びます(アライメント要件は多少異なりますが)。メモリ中心の考え方からそれらを見ていない場合、プログラミング言語の型システムは、潜在的に隣接するメモリ領域を小さなちらつきの小さなチャンクに分割/分離したいです。
スタックはこのメモリ中心のフォーカスを利用してこれを回避し、潜在的にその中にユーザー定義型インスタンスの可能な組み合わせを格納します。スタックを最大限に活用することは可能な限り優れたアイデアであり、その最上部はほとんど常にキャッシュラインにありますが、LIFOパターンなしでこれらの特性の一部を模倣するメモリアロケーターを設計し、異なるデータ型にまたがるメモリを隣接するように融合することもできますより複雑なメモリ割り当ておよび割り当て解除パターンでもチャンク。
最新のハードウェアは、連続するメモリブロックを処理するとき(同じキャッシュライン、同じページなどに繰り返しアクセスするとき)にピークに達するように設計されています。隣接するキーワードは、関心のある周辺データがある場合にのみ有益であるため、連続性があります。そのため、パフォーマンスの鍵(多くの場合、難易度も)は、分離されたメモリチャンクを、エビクション前に全体(関連するすべてのデータ)にアクセスされる連続したブロックに再び融合することです。プログラミング言語の特にユーザー定義型のリッチ型システムはここでの最大の障害になる可能性がありますが、必要に応じてカスタムアロケーターおよび/またはかさばる設計を介して常に問題に対処し、解決できます。
醜い
「Uい」と言うのは難しいです。これは主観的な指標であり、パフォーマンスが非常に重要な分野で働く人は、「美」の考え方をよりデータ指向であり、物事を一括処理するインターフェイスに焦点を当てるものに変え始めるでしょう。
危険な
「危険」の方が簡単かもしれません。一般に、パフォーマンスは低レベルのコードに到達する傾向があります。たとえば、メモリアロケータの実装は、データ型の下に到達し、生のビットとバイトの危険なレベルで作業しないと不可能です。その結果、これらのパフォーマンスクリティカルなサブシステムでの慎重なテスト手順への焦点を高め、最適化のレベルを適用してテストの徹底を拡大するのに役立ちます。
美人
しかし、これらはすべて実装の詳細レベルにあります。ベテランの大規模でパフォーマンス重視の考え方の両方において、「美」は実装の詳細よりもインターフェース設計に移行する傾向があります。インターフェースの設計変更に伴い発生する可能性のある結合やカスケードの破損により、実装よりも「美しく」、使用可能で、安全で、効率的なインターフェースを探すことが、指数関数的に高い優先度になります。実装はいつでも交換できます。通常、必要に応じて、また測定で指摘されているように、パフォーマンスに向かって反復します。インターフェース設計の鍵は、システム全体を破壊することなく、そのような反復の余地を残すために十分に粗いレベルでモデリングすることです。
実際、ベテランのパフォーマンスクリティカルな開発への焦点は、多くのパフォーマンスを持つ大規模なコードベースなので、安全性、テスト、保守性、SEの弟子に重点を置く傾向があることを示唆します。 -重要なサブシステム(パーティクルシステム、画像処理アルゴリズム、ビデオ処理、オーディオフィードバック、レイトレーサー、メッシュエンジンなど)は、メンテナンスの悪夢にdrれるのを防ぐために、ソフトウェアエンジニアリングに細心の注意を払う必要があります。多くの場合、驚くほど効率の良い製品でもバグの数が最も少ないのは偶然ではありません。
TL; DR
とにかく、それは、真にパフォーマンスが重要な分野の優先順位から、待ち時間を短縮し、わずかな非効率性を蓄積させるもの、実際に「美しさ」を構成するもの(最も生産的に見た場合)に至るまで、私がテーマに取り組んでいます。