DirectX11でGPUの作業時間を測定してみた
DirectX11を勉強しながら、サンプルのGPUの各作業時間を測定するため検索してみたところ、ID3D11Query
というのを用いてGPUの時間を測定することがわかりました。
SH-D3D11
サンドボックスプロジェクトのサンプル1と2を実装しながらID3D11Query
でどうやって測定してのかちょっとメモしたいと思います。
本論に入る前に、GPUの作業時間の測定をCPUの作業時間の測定値として一緒に扱ってはいけません。なぜならCPUのプラットフォームで提供している各種の時間測定APIはGPUを考慮しないからです。たとえQueryPerformanceCounter
またはstd::chrono::...::now()
で最初時間と最後時間を測定したとしても、GPUはCPUとは別として動いてますからこの測定値数が本当に信頼性あるかわかりません。単にFPS
でこれをOFFしたら10FPSが上がったというようにパフォーマンスを測定してはいけません。
まずID3D11Query
を使用するにはこのクエリタイプのインスタンスを作らなければなりません。ID3D11Device::CreateQuery
で簡単に作ることが出来ます。
ここでディスクリプタにQueryの種類を書きますが、GPUのクロックを測定するためにはD3D11_QUERY_TIMESTAMP
とD3D11_QUERY_TIMESTAMP_DISJOINT
が個別で必要です。
D3D11_QUERY_TIMESTAMP_DISJOINT
のQueryは、後で説明するID3D11DeviceContext::Begin
とID3D11DeviceContext::End
を順に呼び出す時に必要になります。D3D11_QUERY_TIMESTAMP
はID3D11DeviceContext::End
を呼び出す時のみ必要です。MSDNによるとこのタイプのクエリで、Begin
は無効になるそうです。
ID3D11DeviceContext
の上記のAPIを実行すると、GPUのクロックを記録するようになります。即ち、各自がストップウォッチみたいに動くわけではなく、Disjoint
クエリにより相対的なGPUクロック数が記録されるわけです。
D3D11_QUERY_TIMESTAMP_DISJOINT
のクエリは、Begin
とEnd
によってGPUデバイスのクロック周波数を渡されます。これにより補助のタイムスタンプクエリから記録したクロックに用いて実際の時間として変換することが出来ます。また、これはあくまでもGPUからのコマンドによる実行になりますのでCPUとの時差などに気にしなくても良いそうです。
僕のサンプルコードはこう書いて、Disjointと一般Timestampクエリを作成しました。
// Make timestamp queries (disjoint and start-end queries) // https://docs.microsoft.com/ko-kr/windows/desktop/api/d3d11/nf-d3d11-id3d11device-createquery IComOwner<ID3D11Query> ownDisjointQuery = *FD3D11Factory::CreateTimestampQuery(mD3DDevice.Get(), true); IComOwner<ID3D11Query> ownGpuStartFrameQuery = *FD3D11Factory::CreateTimestampQuery(mD3DDevice.Get(), false); IComOwner<ID3D11Query> ownGpuEndFrameQuery = *FD3D11Factory::CreateTimestampQuery(mD3DDevice.Get(), false); std::optional<IComOwner<ID3D11Query>> FD3D11Factory::CreateTimestampQuery( ID3D11Device& device, bool isDisjoint) { IComOwner<ID3D11Query> result = nullptr; D3D11_QUERY_DESC queryDesc; queryDesc.Query = isDisjoint == true ? D3D11_QUERY_TIMESTAMP_DISJOINT : D3D11_QUERY_TIMESTAMP; queryDesc.MiscFlags = 0; if (device.CreateQuery(&queryDesc, &result) != S_OK) { result.Release(); return std::nullopt; } return result; }
作成されたクエリはこのように実行出来ます。
auto gpuTime = MTimeChecker::CheckGpuD3D11Time( "GpuFrame", ownDisjointQuery.Get(), mD3DImmediateContext.Get(), false); { auto fragment = gpuTime.CheckFragment("Overall", ownGpuStartFrameQuery.Get(), ownGpuEndFrameQuery.Get()); mD3DImmediateContext->ClearRenderTargetView( &mRenderTargetView.Get(), std::array<FLOAT, 4>{bgCol.X, bgCol.Y, bgCol.Z, 1}.data()); mD3DImmediateContext->ClearDepthStencilView(&mDepthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0); MGuiManager::Render(); // Present the back buffer to the screen. HR(mD3DSwapChain->Present(1, 0)); } // RAII! // And gpuTime will be destroyed and RAII happen.
私のサンプルのフレームワークでは素のままじゃなくてラッピングしてから、別のタイプのインスタンスをRAIIとして渡されて利用する形になります。gpuTime
とfragment
がそうですが、このインスタンスは始発のクエリと終わりのクエリを持っていて、RAIIにより自動的にBegin
とEnd
を呼ぶことになります。(Disjoint
クエリのみBegin
を呼び出すことになっています)
そしてDisjoint
クエリのRAIIインスタンスが消滅になると、補助のタイムスタンプクエリを終了することと同時にクエリから貰ったGPUクロックを実際時間に変換します。
std::unordered_map<std::string, double> FD3D11TimeHandle::CalculateTimestamps( ID3D11DeviceContext& dc, ID3D11Query& disjoint, TTimeFragments& fragments) { std::unordered_map<std::string, double> results; // Stall until disjointQuery is available. while (dc.GetData(&disjoint, nullptr, 0, 0) == S_FALSE) ; // Check whether timestamps were disjoint during the last frame D3D11_QUERY_DATA_TIMESTAMP_DISJOINT tsDisjoint; dc.GetData(&disjoint, &tsDisjoint, sizeof(tsDisjoint), 0); if (tsDisjoint.Disjoint) { return results; } for (auto& [fragmentName, pair] : fragments) { auto& [start, end] = pair; // Get All the timestamps. UINT64 tsBeginFrame, tsEndFrame; dc.GetData(start, &tsBeginFrame, sizeof(UINT64), 0); dc.GetData(end, &tsEndFrame, sizeof(UINT64), 0); // Convert to real time (ms). const auto msGpuFrame = double(tsEndFrame - tsBeginFrame) / double(tsDisjoint.Frequency) * 1'000.0; results.try_emplace(fragmentName, msGpuFrame); } return results; }
result
は各タイムスタンプクエリ(2つのクエリの参照を持つ)の作業時間をms
として表したものをハッシュマップに入れたものとなります。これにより、ID3D11Query
を使用して各GPU作業の処理時間を求められるようになりました。
ちなみに
GPUのクロック数値を得るにはDisjointクエリの作業が終わらなければなりません。この為、上のコードではSpinlockみたいなコードを書いてDisjointが終わったかを検証してから値を得ます。
while (dc.GetData(&disjoint, nullptr, 0, 0) == S_FALSE) ;
こういったコードはCPUのStallを起こす恐れがありますので、スレッドを分けてプロファイリングスレッドで独自的に測定させるのがいいかもしれませんね。またこうなると、値を書き下ろすにはAtomicな演算を行わなければならなくなりますし、せめてBackbufferの測定値などを実装してより安全にGPU処理時間値を測らなければならなさそうです。(こっちのプロジェクトではまだやっていませんが、サンプルが複雑になるとやってみる考えはあります)