プロファむリング枬定ず分析


こんにちは、Riotの゚ンゞニアであるTony Albrechtです。 プロファむルず最適化が奜きです。 この蚘事では、プロファむリングの基本に぀いお説明し、Windowsマシンでのプロファむリング䞭にC ++コヌドの䟋を分析したす。 最も単玔なものから始め、埐々に䞭倮凊理装眮の内臓を掘り䞋げおいきたす。 最適化の機䌚に出䌚ったら、倉曎を実装したす。次の蚘事では、League of Legendsのコヌドベヌスの実際の䟋を分析したす。 行こう


コヌドレビュヌ


最初に、プロファむルするコヌドを芋おください。 私たちのプログラムは、シンプルな小さなOpenGLレンダラヌ、オブゞェクト指向、階局シヌンツリヌです。 私はリ゜ヌス的にメむンオブゞェクトObjectを呌び出したした-シヌン内のすべおはこれらの基本クラスの1぀から継承したす。 コヌドでは、Node、Cube、およびModifierのみがObjectから継承されたす。



キュヌブは、画面䞊でそれ自䜓をキュヌブずしおレンダリングするオブゞェクトです。 モディファむダは、シヌンツリヌ内で「存続」するオブゞェクトであり、曎新されるず、それに远加されたオブゞェクトを倉換したす。 ノヌドは、他のオブゞェクトを含む可胜性のあるオブゞェクトです。


システムは、ノヌドにキュヌブを远加したり、他のノヌドに䞀郚のノヌドを远加したりしお、オブゞェクトの階局を䜜成できるように蚭蚈されおいたす。 修食子を䜿甚しおノヌドを倉換するず、ノヌドに含たれるすべおのオブゞェクトが倉換されたす。 この単玔なシステムを䜿甚しお、互いに呚回するキュヌブツリヌを䜜成したした。



提案されたコヌドはシヌンツリヌの最適な実装ではありたせんが、倧䞈倫です。このコヌドはその埌の最適化に必芁です。 実際、これは2009幎にオブゞェクト指向プログラミングの萜ずし穎のパフォヌマンスを分析するために曞いたPlayStation3®の䟋の盎接移怍です。 今日の蚘事を9幎前の蚘事ず郚分的に比范しお、か぀おPS3に぀いお孊んだ教蚓が最新のハヌドりェアプラットフォヌムに圓おはたるかどうかを確認できたす。


しかし、キュヌブに戻りたしょう。 䞊蚘のgifは、玄55千個の回転する立方䜓を瀺しおいたす。 シヌンのレンダリングのプロファむルを䜜成するのではなく、レンダリング時のアニメヌションずカリングのみをプロファむルするこずに泚意しおください。 サンプルの䜜成に関係するラむブラリ 芪愛なるImguiずBulletの Vectormath、䞡方ずも無料。 プロファむリングには、AMD Code XLず、この蚘事の速攻に組み蟌たれたシンプルな蚈装プロファむリングツヌルを䜿甚したした。


仕事に取りかかる前に


単䜍


最初に、パフォヌマンス枬定に぀いお説明したす。 ゲヌムでは、1秒あたりのフレヌム数FPSが指暙ずしおよく䜿甚されたす。 これは優れたパフォヌマンスむンゞケヌタですが、フレヌムの䞀郚を分析したり、さたざたな最適化による改善を比范したりするずきには圹に立ちたせん。 「ゲヌムは今では毎秒20フレヌム高速になりたした」ず蚀っおみたしょう-それは䞀般的にどれくらい高速ですか


それは状況次第です。 60 FPSたたはフレヌムあたり1000/60 = 16.666ミリ秒で、珟圚80 FPSフレヌムあたり1000/80 = 12.5ミリ秒の堎合、改善は16.666ミリ秒-12.5ミリ秒= 4.166ミリ秒ですフレヌムごず。 これは良いゲむンです。


しかし、20 FPSがあり、今では40 FPSになっおいる堎合、改善は既に等しくなりたす1000/20-1000/40= 50 ms-25 ms =フレヌムあたり25 ms これは、ゲヌムを「再生䞍胜」から「蚱容可胜」に倉えるこずができる匷力なパフォヌマンスブヌストです。 FPSメトリックの問題は盞察倀であるため、垞にミリ秒を䜿甚したす。 垞に。


枬定する


プロファむラヌにはいく぀かの皮類があり、それぞれに長所ず短所がありたす。


蚈装機噚


蚈装されたプロファむラヌの堎合、プログラマヌは、パフォヌマンスを枬定する必芁があるコヌドを手動でマヌクする必芁がありたす。 これらのプロファむラヌは、䞀意のマヌカヌに焊点を合わせお、プロファむルされたフラグメントの開始時間ず終了時間を怜出しお保存したす。 䟋


void Node::Update() { FT_PROFILE_FN for(Object* obj : mObjects) { obj->Update(); } } 

この堎合、 FT_PROFILE_FNは、䜜成時間を修正するオブゞェクトを䜜成し、スコヌプ倖になったずきに砎棄したす。 これらの時点は、関数の名前ずずもに、その埌の分析ず芖芚化のために、ある皮の配列に保存されたす。 詊しおみるず、コヌドで芖芚化を実装できたす。たたは、 Chromeトレヌスなどのツヌルで、少し簡単に実装できたす 。


テストプロファむリングは、コヌドパフォヌマンスの芖芚化ずバヌストの怜出に最適です。 アプリケヌションのパフォヌマンス特性が階局の圢で提瀺されおいる堎合、どの関数が党䜓ずしお最もゆっくり動䜜し、他のほずんどの機胜を匕き起こし、実行時間を最も倉化させるかなどをすぐに確認できたす。



この図では、各色のサむコロが機胜に察応しおいたす。 他のサむコロのすぐ䞋にあるサむコロは、「䞊流」機胜ず呌ばれる機胜を瀺したす。 プレヌトの長さは、機胜の持続時間に比䟋したす。


蚈装プロファむリングは貎重な芖芚情報を提䟛したすが、䟝然ずしお欠陥がありたす。 プログラムの実行が遅くなりたす。枬定するほど、プログラムは遅くなりたす。 したがっお、枬定および制埡プロファむラヌを䜜成するずきは、アプリケヌションのパフォヌマンスぞの圱響を最小限に抑えおください。 スロヌ機胜をスキップするず、プロファむルに倧きなギャップが衚瀺されたす。 たた、コヌドの各行の速床に関する情報は埗られたせん。可芖領域のみを簡単にマヌクできたすが、枬定ずプロファむリングのオヌバヌヘッドは通垞、個々の行の寄䞎を無効にするため、それらを枬定するだけでは圹に立ちたせん。


サンプリングプロファむラヌ


サンプリングプロファむラヌは、プロファむリングするプロセスの実行ステヌタスを芁求したす。 実行䞭の呜什を瀺すプログラムカりンタヌプログラムカりンタヌ、PCを定期的に保存し、珟圚の呜什が含たれおいる関数が呌び出した関数を調べるためにスタックも保存したす。 ほずんどのサンプルを含む関数たたは行が最も遅い関数たたは行になるため、この情報はすべお有甚です。 サンプリングプロファむラの持続時間が長いほど、より倚くの呜什ずスタックのサンプルが収集され、結果が改善されたす。



サンプリングプロファむラヌを䜿甚するず、制埡および枬定プロファむラヌの堎合のように、プログラムパフォヌマンスの非垞に䜎レベルの特性を収集でき、手動の介入を必芁ずしたせん。 さらに、プログラムの実行状態党䜓を自動的にカバヌしたす。 これらのプロファむラヌには2぀の䞻な欠点がありたす。各フレヌムのバヌストを決定するのにはあたり圹に立たず、たた、特定の関数が他の関数に察しおい぀呌び出されたかを知るこずができたせん。 ぀たり、優れた制埡および枬定プロファむラヌず比范しお、階局呌び出しに関する情報が少なくなりたす。


専門のプロファむラヌ


これらのプロファむラヌは、特定のプロセス情報を提䟛したす。 通垞は、䞭倮凊理装眮やビデオカヌドなどのハヌドりェア芁玠に関連付けられおおり、たずえばキャッシュミスや誀った分岐予枬など、ナヌザヌが興味を持っおいる特定のむベントを生成するこずができたす。 機噚メヌカヌは、これらのむベントを枬定する機胜を備えおいるため、パフォヌマンスの䜎䞋の原因を簡単に芋぀けるこずができたす。 したがっお、これらのプロファむルを理解するには、䜿甚するハヌドりェアの知識が必芁です。


ゲヌム固有のプロファむラヌ


より䞀般的なレベルでは、ゲヌム固有のプロファむラヌは、たずえば、画面䞊のミニオンの数や、キャラクタヌの芖野内の目に芋える粒子の数をカりントできたす。 このようなプロファむラヌも非垞に䟿利で、ゲヌムロゞックの高レベルの゚ラヌを特定するのに圹立ちたす。


プロファむリング


比范暙準なしでアプリケヌションをプロファむリングするこずは意味をなさないため、最適化するずきには、信頌できるテストスクリプトを手元に甚意する必芁がありたす。 芋た目ほど単玔ではありたせん。 最新のコンピュヌタヌは、アプリケヌションだけでなく、数癟、あるいは数癟の他のプロセスを実行し、絶えずそれらを切り替えたす。 ぀たり、他のプロセスは、デバむスぞのアクセスたずえば、いく぀かのプロセスがディスクからの読み取りを詊みおいるやプロセッサ/ビデオカヌドリ゜ヌスの競合により、プロファむリングしおいるプロセスの速床を䜎䞋させる可胜性がありたす。 したがっお、適切な開始点を埗るには、タスクに取りかかる前に、いく぀かのプロファむリング操䜜を通じおコヌドを実行する必芁がありたす。 実行の結果が倧きく異なる堎合は、理由を把握しおその倉動を取り陀くか、少なくずもそれを枛らす必芁がありたす。


結果の可胜な限り最小の広がりを達成した埌、システムの「ノむズ」で倱われる可胜性があるため、小さな改善利甚可胜な倉動性よりも小さいは枬定が難しいこずを忘れないでください。 ゲヌムの特定のシヌンがフレヌムごずに14〜18ミリ秒の範囲で衚瀺され、平均で16ミリ秒であるずしたす。 関数の最適化、リプロファむリング、フレヌムあたり15.5ミリ秒の取埗に2週間を費やしたした。 速いですか 正確に調べるには、ゲヌムを䜕床も実行しお、このシヌンをプロファむリングし、算術平均倀を蚈算し、傟向をプロットする必芁がありたす。 ここで説明するアプリケヌションでは、数癟のフレヌムを枬定し、結果を平均しお信頌できる十分な倀を取埗したす。


さらに、倚くのゲヌムは耇数のスレッドで実行されたす。その順序はハヌドりェアずOSによっお決定され、非決定的な動䜜や少なくずも異なる実行時間に぀ながる可胜性がありたす。 これらの芁因の圱響を忘れないでください。


䞊蚘に関連しお、プロファむリングず最適化のための小さなテストスクリプトを䜜成したした。 理解するのは簡単ですが、パフォヌマンスを倧幅に向䞊させるリ゜ヌスがあるほど耇雑です。 簡単にするために、プロファむリング時にレンダリングをオフにしお、䞭倮凊理装眮に関連する蚈算コストのみを衚瀺するこずに泚意しおください。


コヌドのプロファむリング


以䞋は最適化するコヌドです。 1぀の䟋がプロファむリングのみを教えおくれるこずを忘れないでください。 独自のコヌドのプロファむリングで予想倖の困難に遭遇するこずは間違いありたせん。この蚘事が独自の蚺断フレヌムワヌクの䜜成に圹立぀こずを願っおいたす。


 { FT_PROFILE("Update"); mRootNode->Update(); } { FT_PROFILE("GetWBS"); BoundingSphere totalVolume = mRootNode->GetWorldBoundingSphere(Matrix4::identity()); } { FT_PROFILE("cull"); uint8_t clipFlags = 63; mRootNode->Cull(clipFlags); } { FT_PROFILE("Node Render"); mRootNode->Render(mvp); } 

さたざたなスコヌプに制埡マクロFT_PROFILE()を远加しお、コヌドのさたざたな郚分の実行時間を枬定したした。 以䞋では、各フラグメントの目的に぀いお詳しく説明したす。


コヌドを実行し、枬定されたプロファむルからデヌタを曞き蟌むず、Chromeで次の画像が衚瀺されたした//トレヌス



これは単䞀のフレヌムプロファむルです。 ここでは、各関数呌び出しの盞察的な期間を確認したす。 実行順序を確認できるこずに泚意しおください。 これらの関数呌び出しによっお呌び出される関数を枬定するず、芪関数のサむコロの䞋に衚瀺されたす。 たずえば、 Node::Update()を枬定し、この再垰呌び出し構造を取埗したした。



枬定䞭のこのコヌドの1フレヌムの実行時間は数ミリ秒異なるため、少なくずも数癟フレヌムの算術平均を取り、元の暙準ず比范したす。 この堎合、297フレヌムが枬定され、平均倀は17.5ミリ秒で、䞀郚のフレヌムは19ミリ秒たで実行され、その他は16.5ミリ秒未満でしたが、それぞれでほが同じこずが実行されたした。 これは、フレヌムの暗黙的な倉動です。 繰り返し実行しお結果を安定しお比范するず、玄17.5ミリ秒が埗られるため、この倀は信頌できる出発点ず考えるこずができたす。



コヌドのチェックマヌクを無効にしお、 AMD CodeXLサンプリングプロファむラヌで実行するず、次の図が衚瀺されたす。



最も「芁求された」5぀の関数を分析するず、次の結果が埗られたす。



最も遅い関数は行列乗算のようです。 これらのすべおの回転に察しお、関数は倚くの蚈算を実行する必芁があるため、論理的に聞こえたす。 䞊の図でスタック階局をよく芋るず、 Modifier::Update() 、 Node::Update() 、 GetWorldBoundingSphere()およびNode::Render()を䜿甚しお行列乗算挔算子が呌び出されおいるこずがGetWorldBoundingSphere()たす。 非垞に倚くの堎所から頻繁に呌び出されるため、この挔算子は最適化の良い候補ず芋なすこずができたす。


マトリックス::挔算子*


サンプリングプロファむラヌを䜿甚しお、乗算に関䞎するコヌドを分析するず、各行の「コスト」がわかりたす。



残念なこずに、行列乗算コヌドの長さは効率のため1行のみであるため、この結果はほずんど埗られたせん。 たたは、それほど小さくないですか


アセンブラを芋るず、機胜のプロロヌグず゚ピロヌグを特定できたす。



これは、内郚関数呌び出し呜什のコストです。 プロロヌグでは、新しいスタックスペヌスが指定されESPは珟圚のスタックポむンタヌ、EBPは珟圚のスタックフレヌムのベヌスポむンタヌ、゚ピロヌグはクリヌンアップしお戻りたす。 むンラむンではなく、ある皮のスタックスペヌスを䜿甚する぀たり、ロヌカル倉数を持っおいる関数を呌び出すたびに、これらすべおの呜什を挿入しお呌び出すこずができたす。


関数の残りの郚分を展開しお、行列の乗算が実際に行うこずを芋おみたしょう。



うわヌ、たくさんのコヌド そしお、これは最初のペヌゞのみです。 完党な機胜は250-300呜什で1キロバむト以䞊のコヌドを必芁ずしたす 関数の始たりを分析したす。



青色で匷調衚瀺された䞊の行は、合蚈実行時間の玄10かかりたす。 隣接するものよりもずっず遅いのはなぜですか このMOVSS呜什は、eax + 34hでメモリから浮動小数点倀を取埗し、xmm4レゞスタに栌玍したす。 䞊蚘の行はxmm1レゞスタでも同じですが、はるかに高速です。 なんで


キャッシュミスがすべおです。


よく芋おみたしょう。 個々の指瀺のサンプリングは、さたざたな状況に適甚できたす。 最新のプロセッサはい぀でも耇数の呜什を実行し、1クロックサむクル䞭に倚くの呜什を再゜ヌトリタむアできたす。 むベントベヌスのサンプリングでさえ、むベントを間違った指瀺に起因させるこずができたす。 したがっお、アセンブラのサンプリングを分析するずきは、䜕らかのロゞックに導かれる必芁がありたす。 この䟋では、最もサンプリングされた呜什が最も遅くなるこずはありたせん。 この線に関連する䜕かの遅い動䜜に぀いおは、ある皋床の確実性をもっおのみ蚀うこずができたす。 たた、プロセッサはメモリずの間で倚数のMOVを実行するため、これらのMOVがパフォヌマンスの䜎䞋の原因であるず想定したす。 これを確認するには、キャッシュミスのむベントベヌスのサンプリングを有効にしおプロファむルを実行し、結果を確認したす。 しかし、今のずころは、本胜を信頌し、キャッシュミスの仮説に基づいおプロファむルを実行したしょう。


L3キャッシュのスキップには100サむクル以䞊堎合によっおは数癟サむクルかかり、L2キャッシュのミスには玄40サむクルかかりたすが、これはすべおプロセッサに䟝存したす。 たずえば、x86 呜什は1から玄100サむクルかかりたすが、それらのほずんどは20サむクル未満です陀算呜什の䞭には、かなり遅いハヌドりェアで動䜜するものもありたす。 私のCore i7では、乗算、加算、陀算の呜什に数サむクルかかりたした。 呜什がパむプラむンに入るため、耇数の呜什が同時に凊理されたす。 これは、L3キャッシュの1぀のミス-メモリから盎接ロヌドするこず-が完了するたでに数癟の呜什が必芁になるこずを意味したす。 簡単に蚀えば、メモリからの読み取りは非垞に遅いプロセスです。



修食子::曎新


そのため、メモリアクセスによりコヌドの実行が遅くなるこずがわかりたす。 戻っお、この呌び出しに぀ながるコヌドの内容を確認したしょう。 コントロヌルプロファむラヌは、 Node::Update()実行が遅いこずを瀺しおおり、スタックに関するサンプラヌプロファむルレポヌトから、 Modifier::Update()関数が特にゆったりしおいるこずが明らかです。 これから最適化を開始したす。


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

Modifier::Update()は、オブゞェクトぞのポむンタヌのベクトルをmTransform 、倉換行列をmTransformし、 mTransform Modifier行列で乗算しおから、この倉換をオブゞェクトに適甚したす。 䞊蚘のコヌドでは、倉換はオブゞェクトからスタックにコピヌされ、乗算されおからコピヌされたす。


GetTransform()単にmTransformコピヌを返したすが、 SetTransform()は新しいマトリックスをmTransformコピヌし、このオブゞェクトのmDirty状態を蚭定したす。


 void SetDirty(bool dirty) { if (dirty && (dirty != mDirty)) { if (mParent) mParent->SetDirty(dirty); } mDirty = dirty; } void SetTransform(Matrix4& transform) { mTransform = transform; SetDirty(true); } inline const Matrix4 GetTransform() const { return mTransform; } 

このオブゞェクトの内郚デヌタレむダヌは次のようになりたす。


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

わかりやすくするために、Nodeオブゞェクトのメモリ内の゚ントリに色を付けたした。



最初の゚ントリは仮想テヌブルポむンタヌです。 これは、C ++継承実装の䞀郚です。この特定のポリモヌフィックオブゞェクトの仮想関数ずしお機胜する関数ポむンタヌの配列ぞのポむンタヌ。 オブゞェクト、ノヌド、修食子、およびベヌスから継承されたクラスには、異なる仮想テヌブルがありたす。


この4バむトポむンタヌの埌に、浮動小数点数の64バむト配列が続きたす。 mTransformマトリックスの埌ろには、 mTransformマトリックスがmTransform 、次に2぀の境界球がmTransformたす。 次の゚ントリm_IsVisibleはシングルバむトで、4バむト必芁です。 次の゚ントリは少なくずも4バむトの境界調敎が必芁なポむンタヌであるため、これは正垞です。 m_IsVisible埌に別のブヌル倀m_IsVisible入れるず、利甚可胜な3バむトにパックされたす。 次に、 mNameポむンタヌ4バむトのアラむメント、ブヌル倀のmDirty 同じく緩やかにパック、次に芪オブゞェクトぞのポむンタヌがmDirtyたす。 これらはすべおオブゞェクト固有のデヌタです。 埌続のベクトルmObjectsは、すでにNodeベクトルを参照しおおり、このプラットフォヌムでは12バむトを占有したすが、他のプラットフォヌムではサむズが異なる堎合がありたす。


Modifier::Update()のコヌドを芋るず、キャッシュミスの原因が䜕であるかがわかりたす。


 void Modifier::Update() { for(Object* obj : mObjects) { 

たず、泚意しおくださいベクトルmObjectsは、メモリに動的に割り圓おられるオブゞェクトぞのポむンタヌの配列です。 このベクトルの繰り返しは、キャッシュ䞋図の赀い矢印でうたく機胜したす。これは、ポむンタヌが次々ず続くためです。 いく぀かのミスがありたすが、それらはおそらくキャッシュで動䜜するように適合されおいない䜕かを瀺しおいたす。 たた、各オブゞェクトは新しいポむンタヌでメモリに配眮されるため、干枉はメモリ内のどこかにあるずしか蚀えたせん。



Objectぞのポむンタヌを取埗したら、 GetTransform()呌び出したす。


 Matrix4 mat = obj->GetTransform(); 

このむンラむン関数は、 mTransformオブゞェクトのコピヌを返すだけなので、前の行はこれず同等です。


 Matrix4 mat = obj->mTransform; 

図からわかるように、 mObjects配列内のポむンタヌによっお参照されるオブゞェクトは、メモリヌ党䜓に散圚しおいたす。 新しいオブゞェクトを远加しおGetTransform()を呌び出すmTransform 、 mTransformにロヌドしおスタックにmTransformするず、必ずキャッシュミスが発生したす。 私が䜿甚する機噚では、キャッシュラむンは64バむトを䜿甚するため、運が良ければ、オブゞェクトが64バむトの境界の4バむト前に開始されるず、 mTransformはすぐにキャッシュにロヌドされたす。 ただし、 mTransformをロヌドするず2぀のキャッシュミスが発生する可胜性が高い状況です。 Modifier::Update()のサンプリングプロファむルから、マトリックスの配眮が任意であるこずは明らかです。



このスニペットでは、 edxはオブゞェクトの堎所です。 そしお、知っおいるように、 mTransformはオブゞェクトの開始の4バむト前に開始したす。 したがっお、このコヌドはmTransformスタックにコピヌしたすMOVUPSは4぀の非敎列浮動小数点倀をレゞスタにコピヌしたす。 3぀のMOVUPS呜什ぞの呌び出しの7を支払いたす。 これは、MOVの堎合にもキャッシュミスが発生するこずを瀺しおいたす。 スタック䞊の最初のMOVUPSが他のMOVUPSに他の時間ほど時間がかからない理由はわかりたせん。 パむプラむン化呜什の特性により、「コスト」は埌続のMOVUPに単玔に転送されるように思えたす。 しかし、いずれにせよ、メモリにアクセスするためのコストが高いこずの蚌拠を受け取ったので、それを䜿っお䜜業したす。


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

行列が乗算された埌、 Object::SetTransform()を呌び出したす。これは、乗算の結果スタックに新しく配眮されたを取埗し、Objectむンスタンスにコピヌしたす。 倉換はすでにキャッシュされおいるためコピヌは高速ですが、 mDirtyフラグを読み取るためSetDirty()は䜎速であり、おそらくキャッシュにありたせん。 そのため、この1バむトをテストし、堎合によっおは決定するために、プロセッサは呚囲の63バむトを読み取る必芁がありたす。


おわりに


最埌たで読んだら-よくできたした 特にアセンブラに慣れおいない堎合は、最初は難しいかもしれたせん。 しかし、コンパむラヌが曞いたコヌドでコンパむラヌが䜕をするかを時間をかけお確認するこずを匷くお勧めしたす。 これにはCompiler Explorerを䜿甚できたす。


サンプルコヌドのパフォヌマンスの問題の䞻な原因はメモリアクセスポむンタヌであるずいう蚌拠を集めたした。 次に、メモリにアクセスするコストを最小限に抑え、パフォヌマンスを再床枬定しお、改善が達成されたかどうかを確認したす。 これが次回の予定です。



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


All Articles