🌋

VulkanでGPU時間を計測する方法

2023/01/25に公開

VulkanにはGPU処理にかかった時間を計測するための機能があります。

使い方を簡単にまとめると以下のようになります。

  1. vk::QueryPoolを作成する。
  2. timestampPeriodを調べる。
  3. vk::CommandBuffer::writeTimestamp()で開始時のタイムスタンプを記録する。
  4. なにかしら計測したい処理をコマンドバッファに積む。
  5. 終了時のタイムスタンプを記録する。
  6. GPUに処理を投げる。
  7. vk::Device::getQueryPoolResults()で計測結果を取得する。

実装

まずはQuery poolを作成します。

vk::PhysicalDevice physicalDevice = pickPhysicalDevice();
vk::UniqueDevice device = createDevice();

vk::QueryPoolCreateInfo queryPoolInfo;

// GPU時間計測にはタイムスタンプを使う
queryPoolInfo.setQueryType(vk::QueryType::eTimestamp); 

// フレームバッファ数(3)ごとに開始時と終了時の2つのQueryを使う
// |frame 0|frame 1|frame 2| <- 3 framebuffers
// | s | e | s | e | s | e | <- 6 queries
queryPoolInfo.setQueryCount(6);  

vk::UniqueQueryPool queryPool = device->createQueryPoolUnique(queryPoolInfo);

次に使用しているGPUのtimestampPeriodを調べます。これはタイムスタンプ1単位に掛かる時間をナノ秒で表します。

// timestampPeriodを調べる
float timestampPeriod = physicalDevice.getProperties().limits.timestampPeriod;

これで事前準備が終了したので、ここからはゲームループ内で実際に計測していきます。

std::array<uint64_t, 6> timestamps{}; // 結果を格納する配列を用意
while(true) {
  uint32_t frameIndex = acquireNextImage();
  vk::CommandBuffer commandBuffer = getCurrentCommandBuffer();
  
  // 使う前にはリセットする必要がある
  commandBuffer.resetQueryPool(*queryPool,
                               0, // firstQuery
			       2  // queryCount
			       );
  
  // 現在フレームの開始タイムスタンプを書き込む
  commandBuffer.writeTimestamp(vk::PipelineStageFlagBits::eTopOfPipe, 
                               *queryPool, 
			       frameIndex * 2 // query
			       );
  
  // 計測したい処理をコマンドバッファに積む
  // サンプルとして適当なレンダリングをする
  commandBuffer.beginRenderPass();
  commandBuffer.bindVertexBuffers();
  commandBuffer.bindIndexBuffer();
  commandBuffer.drawIndexed();
  commandBuffer.endRenderPass();
  
  // 終了タイムスタンプを書き込む
  commandBuffer.writeTimestamp(vk::PipelineStageFlagBits::eBottomOfPipe, 
                               *queryPool, 
			       frameIndex * 2 + 1 // query
			       );
  
  // GPUに処理を投げる
  queue.submit();
  
  // 2つのタイムスタンプを取得する
  // e64フラグを使うとuint64_tで結果を返す(デフォルトはuint32_tなので小さすぎる)
  // eWaitフラグを使うと待機する(GPUに投げた直後なので処理が終わるのを待つ必要がある)
  // 当然待機するとFPSが下がるため、うまく管理して次以降のフレームで結果取得する方がいい
  device->getQueryPoolResults(*queryPool, 
                              frameIndex * 2,                       // firstQuery
                              2,                                    // queryCount
                              timestamps.size() * sizeof(uint64_t), // dataSize
                              timestamps.data(),                    // pData
                              sizeof(uint64_t),                     // stride
                              vk::QueryResultFlagBits::e64 | 
			      vk::QueryResultFlagBits::eWait);
  
  // 経過時間(ns)を計算する
  float elapsedTime = 
    timestampPeriod * static_cast<float>(timestamps[frameIndex * 2 + 1] - timestamps[frameIndex * 2]);
  Log::info("GPU time: {}ns", elapsedTime);
}

以上です。

結果

こんな感じに出力できるようになりました。

参考

より詳しい説明が知りたい方はこちらのブログを参照してください。

https://nikitablack.github.io/post/how_to_use_vulkan_timestamp_queries/

Khronos公式の実装を眺めたい方はこちらを。

https://github.com/KhronosGroup/Vulkan-Samples/blob/e3023c3d108bedd27de6039065e3aed5bac96e8e/framework/core/query_pool.cpp

https://github.com/KhronosGroup/Vulkan-Samples/blob/e3023c3d108bedd27de6039065e3aed5bac96e8e/framework/stats/vulkan_stats_provider.cpp

Discussion