プロファむリング最適化


これは、コヌドの最適化に関する䞀連の蚘事の2番目の蚘事です。 最初から、パフォヌマンスを䜎䞋させるコヌドのボトルネックを芋぀けお分析する方法を孊びたした。 この䟋の䞻な問題は、メモリアクセスが遅いこずです。 この蚘事では、メモリを操䜜する際のコストを削枛し、プログラムの速床を䞊げる方法を怜蚎したす。


コヌドの最新バヌゞョンは次のずおりです。


void Modifier::Update() { for(Object* obj : mObjects) { Matrix4 mat = obj->GetTransform(); mat = mTransform*mat; obj->SetTransform(mat); } } 

メモリアクセスの最適化


メモリに関する仮定が正しい堎合、いく぀かの方法でプログラムを高速化できたす。



もちろん、最埌のステップから始めたす。 プロセッサ開発者は、メモリぞのアクセスが遅いプロセスであるこずを理解しおおり、機噚を蚭蚈する際にこの欠点を可胜な限り補おうずしたす。 たずえば、必芁ず思われるデヌタは、メモリから事前に取埗されたす。 プログラムが本圓にそれらを芁求するずき、埅機時間は最小限です。 プログラムがデヌタにシヌケンシャルにアクセスする堎合、これは簡単に実装できたすが、ランダムアクセスでは完党に䞍可胜です。 したがっお、配列内のデヌタぞのアクセスは、リンクされたリストからのアクセスよりもはるかに高速です。 ネットワヌクには、キャッシュの操䜜に関する倚くの優れたドキュメントがあるため、高性胜プログラムの䜜成に関心がある堎合は、必ず孊習しおください。 開始するのに圹立぀資料をいく぀か玹介したす。


メモリ内のレむアりトを倉曎する


最適化の最初のステップは、コヌドではなく、メモリのレむアりトメモリレむアりトを倉曎するこずです。 メモリを操䜜する簡単なツヌルを䜜成したした。これにより、特定のタむプのすべおのデヌタが1぀のプヌルで芋぀かり、このデヌタが可胜な限り順番に䜿甚できるようになりたす。 そのため、このむンスタンス内に行列や他の型を保存する代わりに、オブゞェクトを配眮するずきに、そのような型のプヌルにあるデヌタ型ぞのポむンタを保存したす。


その結果、Objectクラスのデヌタストレヌゞは次のように倉曎されたす。


 protected: Matrix4 mTransform; Matrix4 mWorldTransform; BoundingSphere mBoundingSphere; BoundingSphere mWorldBoundingSphere; bool m_IsVisible = true; const char* mName; bool mDirty = true; Object* mParent; 

これに぀いお


 protected: Matrix4* mTransform = nullptr; Matrix4* mWorldTransform = nullptr; BoundingSphere* mBoundingSphere = nullptr; BoundingSphere* mWorldBoundingSphere = nullptr; const char* mName = nullptr; bool mDirty = true; bool m_IsVisible = true; Object* mParent; 

それは私が聞くこずです「しかし今、より倚くのメモリが䜿甚されおいたす ポむンタヌず、それが参照するデヌタ自䜓の䞡方が含たれおいたす。 あなたは正しいです。 もっずメモリが必芁です。 倚くの堎合、最適化は劥協ですパフォヌマンスを向䞊させる代わりに、メモリを増やすか粟床を䞋げたす。 この堎合、別のポむンタヌが远加され32ビットアセンブリの堎合は4バむト、Objectからのポむンタヌに埓っお、倉換する必芁のある実際の行列を芋぀ける必芁がありたす。 はい、これは远加の䜜業ですが、探しおいるマトリックスはObjectむンスタンスから読み取るポむンタヌのように順番に配眮されるため、回路はメモリからランダムな順序で読み取るよりも高速に動䜜するはずです。 これは、オブゞェクト、ノヌド、および修食子が䜿甚するメモリにデヌタを順番に配眮する必芁があるこずを意味したす。 実際のシステムでは、このようなタスクを解決するのは難しい堎合がありたすが、䟋を怜蚎しおいるため、立堎を匷化するためにarbitrary意的な制限を課したす。


オブゞェクトコンストラクタヌは次のようになりたした。


 Object(const char* name) :mName(name), mDirty(true), mParent(NULL) { mTransform = Matrix4::identity(); mWorldTransform = Matrix4::identity(); sNumObjects++; } 

そしお今、このようになりたす


 Object(const char* name) :mName(name), mDirty(true), mParent(NULL) { mTransform = gTransformManager.Alloc(); mWorldTransform = gWorldTransformManager.Alloc(); mBoundingSphere = gBSManager.Alloc(); mWorldBoundingSphere = gWorldBSManager.Alloc(); *mTransform = Matrix4::identity(); *mWorldTransform = Matrix4::identity(); mBoundingSphere->Reset(); mWorldBoundingSphere->Reset(); sNumObjects++; } 

gManager.Alloc()呌び出しは、単玔なブロックアロケヌタヌの呌び出しです。


 Manager<Matrix4> gTransformManager; Manager<Matrix4> gWorldTransformManager; Manager<BoundingSphere> gBSManager; Manager<BoundingSphere> gWorldBSManager; Manager<Node> gNodeManager; 

メモリの倧きなチャンクを事前に割り圓おおから、各Alloc()呌び出しは次のNバむトを返したすMatrix4堎合は64バむト、 Matrix4堎合は32バむト。 ただし、特定のタむプには倧量のメモリが必芁です。 実際、これは、メモリに事前に割り圓おられた特定タむプのオブゞェクトの倧きな配列です。 このようなディストリビュヌタの利点の1぀は、特定の順序で配眮された特定のタむプのオブゞェクトがメモリ内で次々に移動するこずです。 オブゞェクトをアクセスしたい順序で配眮するず、ハヌドりェアはより効率的にプリフェッチしお、指定された順序でオブゞェクトを凊理できたす。 幞いなこずに、この䟋では、メモリに配眮された順序で行列やその他の構造を調べたす。


すべおのオブゞェクト、マトリックス、および境界球が回転順に保存されたので、プログラムのプロファむルを倉曎しお、パフォヌマンスが倉化したかどうかを確認したす。 コヌドぞの唯䞀の倉曎は、メモリレむアりトの倉曎ず、ポむンタを介したデヌタの䜿甚方法であるこずに泚意しおください。 たずえば、 Modifier::Update()は次のようになりたす。


 void Modifier::Update() { for(Object* obj : mObjects) { Matrix4* mat = obj->GetTransform(); *mat = (*mTransform)*(*mat); obj->SetDirty(true); } } 

マトリックスぞのポむンタヌを䜿甚するため、 SetTransformを䜿甚しおマトリックス乗算の結果をオブゞェクトにコピヌする必芁がなくなりたした。すぐに倉換したす。


パフォヌマンスの倉化


倉曎埌、最初にコヌドが正しくコンパむルおよび実行されおいるこずを確認したす。 テストプロファむルの以前の結果は次のずおりです。



しかし、メモリ内のレむアりトを倉曎した埌はどうなりたしたか


぀たり、プログラムをフレヌムあたり17.5ミリ秒から10.15ミリ秒に加速したした メモリ内のデヌタのレむアりトのみを倉曎するず、ほが2倍の速床になりたした。


この䞀連の蚘事の䞻なものを思い出しおください。メモリ内のデヌタのレむアりトは、プログラムのパフォヌマンスにずっお重芁です。 人々は性急な最適化の危険性に぀いお話したすが、私の経隓が瀺唆しおいるように、デヌタず凊理および凊理の順序ずの関係に぀いお少し慎重にすれば、生産性が倧幅に向䞊したす。 事前にレむアりトを蚈画できない堎合は、レむアりトの再線成ず䜿甚に圹立぀コヌドを蚘述しようずするず、最適化が促進されたす。 ほずんどの堎合、スロヌダりンするのはコヌドではなく、デヌタアクセスです。


次のボトルネック


コヌドは高速ですが、それで十分ですか もちろん違いたす。 パフォヌマンス特性が倉曎されたため、プロファむルを再䜜成し、最適化の次の候補を芋぀ける必芁がありたす。 サンプリングプロファむラヌの実行を比范したす。 それは



次のようになりたした



サンプリングプロファむルでは、プログラムが珟圚より高速であるかどうかはわかりたせん。 各機胜に費やされた盞察的な時間のみが衚瀺されたす。 これで、行列挔算子の乗算関数が最も長く機胜したす。 これは、他の人ず比べお速床が䜎䞋したこずを意味するのではなく、完了するたでに時間がかかり始めたした。 サンプリングを芋お、ボトルネックがどこにあるのかを芋おみたしょう。


たず、 Modifier::Update()芋おください。 マトリックスはスタックにコピヌされなくなりたした。 これは、ポむンタヌをマトリックスに枡し、64バむトすべおをコピヌするわけではないため、お勧めです。



関数の時間の倧郚分は、行列乗算の呌び出しの蚭定に費やされ、その結果をobj->mTransformの堎所にコピヌしobj->mTransform 簡略化されたアセンブラヌを以䞋に瀺したす。



問題が発生したす関数を呌び出すコストを削枛する方法は 答えは簡単です。むンラむンにする必芁がありたす。 しかし、行列乗算関数をよく芋るず、むンラむン化の準備がすでに敎っおいるこずが明らかになりたす。 それでは問題は䜕ですか


関数が十分に小さい堎合、コンパむラは倚くの堎合、自動的にむンラむン化するこずを決定したす。 ただし、プログラマはこの動䜜を制埡できたす。 むンラむン関数の最倧サむズを倉曎できたす。䞀郚のコンパむラでは、むンラむンの「匷制」が可胜ですが、これには代䟡がありたす。 コヌドのサむズが倧きくなる可胜性があり、さたざたな堎所からより倚くのバむトを実行するずキャッシュがスリップし始めたす。


SIMDはありたすか


行列乗算挔算子は、SIMDではなく、単䞀の浮動小数点数を䜿甚したす単䞀呜什耇数デヌタ最新のプロセッサは、4぀の浮動小数点数を同時に乗算するなど、デヌタの耇数のコピヌで単䞀の呜什を実行できる呜什を提䟛したす。 SIMDを䜿甚するこずをコンピュヌタヌに䌝えおも、無芖され、同様のコヌドが生成されたす。



SIMD呜什はありたせん。16バむトにアラむメントされたデヌタが必芁であり、4バむトのアラむメントしか持っおいないためです。 もちろん、アラむメントされおいないデヌタをロヌドしおから、それをSIMDレゞスタに転送できたす。 ただし、SIMD凊理の量が倧きすぎない堎合、䞀床に1぀の浮動小数点数を䜿甚する堎合よりも速床が䜎䞋する可胜性がありたす。 幞いなこずに、このようなオブゞェクト甚のメモリマネヌゞャを䜜成したため、マトリックスに16バむトたたはそれ以䞊のアラむメントを匷制できたす。 この䟋では、16に決めたした。たた、VectormathラむブラリヌにSIMD呜什のみを生成させたした。


プロファむラヌの次の実行時に、平均フレヌム期間が10.15から玄7ミリ秒に枛少したこずが刀明したした。



サンプリングプロファむルを芋おみたしょう。



行列乗算挔算子が消えたした。 SIMD呜什を倉換するず、関数のサむズが小さくなり1060バむトから玄350、コンパむラヌはむンラむン化できたした。 行列乗算コヌドはそれを呌び出したすべおの関数に含たれおいたしたが、珟圚では乗算関数のむンスタンスを呌び出したせん。 副䜜甚ずしお、コヌドサむズが倧きくなりたした255の代わりに.exeファむルが256 Kbを占有し始めたため、あたり気にしたせんが、呌び出された関数を䜿甚するために関数のパラメヌタヌをスタックにコピヌする必芁がなくなりたした。


Modifier::Update()詳しくModifier::Update()たしょう。



13行目はmTransformぞのポむンタを取り、その埌すぐに、マトリックスSIMDコヌドはMOVAPS呜什䜍眮合わせされた単粟床浮動小数点倀の移動を䜿甚しおxmmレゞスタに入力したす。 そのため、MULPSMultiply Packed Single-Precision Floating-Point Valuesなどの呜什を䜿甚しお䞀床に4぀の浮動小数点数を凊理するだけでなく、これらの数倀も同時に読み蟌みたす。 その結果、関数内の呜什の数が倧幅に削枛され、パフォヌマンスが向䞊したす。 ただブレヌキがあり、キャッシュミスに起因する可胜性がありたすが、䞻に呜什の䜿甚ずむンラむン化の改善により、生産性が向䞊したした。 コヌドの初期では、行列の乗算がよく呌ばれおいたしたが、今ではかなりの量の関数呌び出しがなくなりたした。


そのため、フレヌムの凊理時間を17.5から玄7ミリ秒に短瞮したした。぀たり、コヌドを劣化させるこずなく2.5倍のパフォヌマンスを向䞊させたした。 耇雑さを倧幅に増加させるこずなく、より速く動䜜し始めたした。 デヌタをメモリに保存する方法を制限したしたが、高レベルのコヌドを倉曎せずに䜎レベルの倉曎を行うこずができたす。


远加の割り圓お仮想機胜


2009幎のプレれンテヌションでは、ここよりもはるかに進んでいたす。コヌドを壊し、すべおのバヌチャルを削陀し、コヌドの柔軟性を䜎䞋させたしたが、パフォヌマンスは2倍になりたした。 今はしたせん。 サンプルをさらに最適化できるず確信しおいたすが、読みやすさ、拡匵性、移怍性、メンテナンスの容易さでこれを支払う必芁がありたす。 代わりに、実装を怜蚌し、仮想機胜の圱響を怜蚎したす。


掟生クラスで再定矩できる基本クラスの仮想関数が呌び出されたす。 たずえば、Objectクラスには、 Render()の機胜を定矩するCube()およびElephant()オブゞェクトを継承および䜜成できる仮想関数Render()がありたす。 これらの新しいオブゞェクトはノヌドの1぀に远加できたす。元のオブゞェクトのポむンタヌを䜿甚しおRender()を呌び出すず、オヌバヌラむドされたRender()関数が呌び出され、画面に立方䜓たたは象が描画されたす。


この䟋の最も簡単なテストは、オブゞェクトずノヌドのSetVisibilityRecursively()メ゜ッドです。 ノヌドが完党に錐台の内偎たたは倖偎にあるカリングフェヌズ䞭に䜿甚されるため、その子は単玔に衚瀺たたは非衚瀺に割り圓おるこずができたす。 Objectの堎合、内郚フラグが蚭定されるだけです


 virtual void SetVisibilityRecursively(bool visibility) { m_IsVisible = visibility; } 

Nodeの堎合、すべおの子に察しおSetVisibilityRecursively()が呌び出されたす。


 void Node::SetVisibilityRecursively(bool visibility) { m_IsVisible = visibility; for(Object* obj : mObjects) { mObjects[i]->SetVisibilityRecursively(visibility); } } 

これは、サンプリングプロファむルを分析するずきにノヌドバヌゞョンに衚瀺されるものです。



ESIはルヌプのカりンタヌであり、EAXは次のオブゞェクトぞのポむンタヌです。 匷調衚瀺されおいる行はCALL DWORD [EAX + 14h]です。ここでは、仮想テヌブルの先頭から0×1410進数の20バむトにある仮想関数が呌び出されたす。 ポむンタヌは4バむトで、すべおの合理的な人が0からカりントを開始するため、これはオブゞェクトの仮想テヌブル内の5番目の仮想関数の呌び出しです。 たたは、以䞋に瀺すように、 SetVisibilityRecursively() 



仮想関数を呌び出すには、もう少し䜜業が必芁です。 仮想テヌブルをロヌドする必芁がありたす。次に、仮想関数を怜玢しお呌び出し、いく぀かのパラメヌタヌず「this」ポむンタヌをそのテヌブルに枡したす。


事実䞊欠けおいるバヌチャル


次のコヌド䟋では、ノヌドずオブゞェクトで、仮想関数を通垞の関数に眮き換えたした。 これは、それぞれが異なるため、各ノヌドに2぀の配列ノヌドずオブゞェクトを保持する必芁があるこずを意味したす。 䟋



ご芧のずおり、仮想テヌブルのダりンロヌドや仮想関数の逆参照はありたせん。 最初のケヌスでは、 Node::SetVisibilityRecursively()実装の単玔な関数呌び出しのみがあり、2番目のケヌスでは、Object実装では、関数は完党にむンラむンです。 コンパむラヌは、実行時にのみ定矩できないため、仮想関数をむンラむン化できたせん。 したがっお、この非仮想実装はより高速に動䜜するはずです。 プロファむルしお調べおみたしょう。



この最適化により、パフォヌマンスはほずんど倉化したせんでした6.961から6.963ミリ秒。 これは、コヌドが0.002ミリ秒遅くなったこずを意味したすか たったくありたせん。 この違いは、システムのノむズ背景に起因する可胜性がありたす。これは、倚くのタスクを同時に実行するOSを実行しおいるパヌ゜ナルコンピュヌタヌですTwitterフィヌドの曎新、メヌルのチェック、閉じ忘れたペヌゞのアニメヌションGIFの曎新。 OSが実行可胜コヌドにどのように圱響するかを理解するために、貎重なWindows Performance Analyzerツヌルを䜿甚できたす。 この゜フトりェアは、コヌドのパフォヌマンスを衚瀺するだけでなく、システム党䜓を党䜓的に衚瀺したす。 そのため、コヌドが突然2倍の長さで凊理を開始した堎合は、他の誰かがこれを犯しおいるかどうかを確認し、プロセッササむクルを盗みたす。 ブルヌス・ドヌ゜ンの優れたブログも読むこずができたす。


したがっお、仮想関数を削陀しおも、コヌドのパフォヌマンスにはほずんど圱響したせん。 仮想テヌブルでの怜玢回数の枛少ずより少ない呜什の実行にもかかわらず、プログラムは同じ速床で実行されたす。 最も可胜性が高いのは、Miracles of Modern Equipmentです-分岐予枬ず投機的実行は、ハッキングだけでなく、繰り返しコヌドを取埗し、可胜な限り迅速に実行する方法を芋぀け出すのにも適しおいたす。


ヘンリヌペトロスキヌを匕甚するには


「珟代の゜フトりェア業界の最も驚くべき成果は、ハヌドりェア業界の着実で芋事な成功の攟棄の継続です。」


仮想関数は無料では䜿甚できないこずを瀺したしたが、この堎合、コストはごくわずかです。


たずめ


サンプルコヌドのプロファむルを䜜成し、ボトルネックを特定し、その圱響を軜枛しようずしたした。 コヌドを倉曎するたびに、プロファむルを再プロファむリングしお、悪化したか改善したかを把握したした゚ラヌは有甚であり、そこから孊習したす。 そしお、各段階で、コヌドの実行方法、メモリぞのアクセス方法をよりよく理解したす。 この゜リュヌションは、高レベルのコヌドを最小限に修正し、単䞀フレヌムのレンダリング時間を17ミリ秒から7ミリ秒に短瞮したす。 メモリが遅いため、ハヌドりェアを操䜜しお高速コヌドを蚘述する必芁があるこずがわかりたした。


シリヌズの第3郚では、 League of Legendsで最近発芋したパフォヌマンスのボトルネックの特定、分析、および最適化に぀いお説明したす。




Source: https://habr.com/ru/post/J351482/


All Articles