如何测量 Vulkan 管道的执行时间
How to measure execution time of Vulkan pipeline
总结
我希望能够测量 运行 整个图形管道在 GPU 上经过的时间(以毫秒为单位)。目标:为了能够保存基准 before/after 优化代码(下一步将是 mipmapping 纹理)以查看改进。这在 OpenGL 中非常简单,但我是 Vulkan 的新手,需要一些帮助。
我浏览了相关的现有答案 ( and ),但它们并没有太大帮助。而且我到处都找不到代码示例,所以我敢在这里问。
通过文档页面,我发现了几个我认为应该使用的函数,所以我准备了这样的东西:
1:正在创建查询池
void CreateQueryPool()
{
VkQueryPoolCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO;
createInfo.pNext = nullptr; // Optional
createInfo.flags = 0; // Reserved for future use, must be 0!
createInfo.queryType = VK_QUERY_TYPE_TIMESTAMP;
createInfo.queryCount = mCommandBuffers.size() * 2; // REVIEW
VkResult result = vkCreateQueryPool(mDevice, &createInfo, nullptr, &mTimeQueryPool);
if (result != VK_SUCCESS)
{
throw std::runtime_error("Failed to create time query pool!");
}
}
我有 queryCount = mCommandBuffers.size() * 2
的想法 space 用于渲染前后的单独查询时间戳,但我不知道这个假设是否正确。
2:记录命令缓冲区
// recording command buffer i:
vkCmdWriteTimestamp(mCommandBuffers[i], VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, mTimeQueryPool, i);
// render pass ...
vkCmdWriteTimestamp(mCommandBuffers[i], VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, mTimeQueryPool, i);
vkCmdCopyQueryPoolResults(/* many parameters here */);
我正在寻找一些说明:
- 写入同一个查询索引的后果是什么?我是否需要两个单独的查询池 - 一个用于渲染时间之前,一个用于渲染时间之后?
- 我应该如何处理同步?我假设每个命令缓冲区都有一个单独的查询。
- 对于包含查询结果的目标缓冲区,存储在“主机可见位”的某个地方是否足够好,或者我是否需要“仅设备可见”的暂存内存?我对这个也有点迷茫。
我无法找到任何关于如何测量渲染时间的在线示例,但我只是假设这是一项如此常见的任务,所以肯定在某处有一个示例。
写入同一个查询索引是不好的,因为您在同一个查询索引处用“之后”时间戳覆盖了“之前”时间戳。您可能希望将写入时间戳调用中的最后一个参数更改为“之前”调用的 i * 2
和“之后”调用的 i * 2 + 1
。您已经为每个命令缓冲区分配了 2 个时间戳,但只使用了其中的一半。该方案最终为每个命令缓冲区 i
.
生成一对 before/after 时间戳
我没有任何使用经验vkCmdCopyQueryPoolResults()
。如果您可以让您的队列空闲,那么在空闲之后,调用 vkGetQueryPoolResults()
这对于您在这里所做的事情来说可能会容易得多。它将查询结果复制回主机内存,您不必将同步写入另一个缓冲区然后 mapping/reading 返回。
所以,感谢@karlschultz,我设法让一些东西工作了。因此,为了防止其他人寻找相同的答案,我决定在此处 post 我的发现。对于那里的 Vulkan 专家:如果我犯了明显的错误,请告诉我,我会在这里纠正它们!
查询池创建
我按照我的问题中的描述填写了一个 VkQueryPoolCreateInfo
结构,并让它的 queryCount
字段等于命令缓冲区数量的两倍,以便在 [之前存储 space 用于查询=65=]和渲染后。
重要的是在使用查询之前重置查询池中的所有条目,和在写入查询后重置查询。这需要进行一些更改:
1) 询问图形队列是否支持时间戳
选择图形队列家族时,structVkQueueFamilyProperties
有一个字段timestampValidBits
必须大于0,否则队列家族不能用于时间戳查询!
2) 确定时间戳周期
物理设备包含一个特殊值,该值指示时间戳查询递增 1 所需的纳秒数。这对于将查询结果解释为例如纳秒或毫秒。该值是 float
,可以通过调用 vkGetPhysicalDeviceProperties
并查看字段 VkPhysicalDeviceProperties.limits.timestampPeriod
.
来检索
3) 请求查询重置支持
在逻辑设备创建过程中,必须填写一个结构并将其添加到pNext
链中以启用主机查询重置功能:
VkDeviceCreateInfo createInfo{};
VkPhysicalDeviceHostQueryResetFeatures resetFeatures;
resetFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_HOST_QUERY_RESET_FEATURES;
resetFeatures.pNext = nullptr;
resetFeatures.hostQueryReset = VK_TRUE;
createInfo.pNext = &resetFeatures;
4) 记录命令缓冲区
时间戳查询应该在渲染过程的范围之外,如下所示。由于管道阶段的(潜在)时间重叠,无法测量单个着色器(例如片段着色器)的 运行 时间,只能测量整个管道或渲染通道范围之外的任何内容。
vkCmdWriteTimestamp(mCommandBuffers[i], VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, mTimeQueryPool, i * 2);
vkCmdBeginRenderPass(/* ... */);
// render here...
vkCmdEndRenderPass(mCommandBuffers[i]);
vkCmdWriteTimestamp(mCommandBuffers[i], VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, mTimeQueryPool, i * 2 + 1);
5) 检索查询结果
我们有两种方法:vkCmdCopyQueryPoolResults
和 vkGetQueryPoolResults
。我选择了后者,因为它大大简化了设置并且不需要与 GPU 缓冲区同步。
鉴于我有一个交换链索引(在我的场景中同样是命令缓冲区索引!),我有这样的设置:
void FetchRenderTimeResults(uint32_t swapchainIndex)
{
uint64_t buffer[2];
VkResult result = vkGetQueryPoolResults(mDevice, mTimeQueryPool, swapchainIndex * 2, 2, sizeof(uint64_t) * 2, buffer, sizeof(uint64_t),
VK_QUERY_RESULT_64_BIT);
if (result == VK_NOT_READY)
{
return;
}
else if (result == VK_SUCCESS)
{
mTimeQueryResults[swapchainIndex] = buffer[1] - buffer[0];
}
else
{
throw std::runtime_error("Failed to receive query results!");
}
// Queries must be reset after each individual use.
vkResetQueryPool(mDevice, mTimeQueryPool, swapchainIndex * 2, 2);
}
变量mTimeQueryResults
指的是std::vector<uint64_t>
,其中包含每个交换链的结果。我使用它通过使用步骤 2 中确定的时间戳周期来计算每秒的平均渲染时间。
并且一定不要忘记通过调用 vkDestroyQueryPool
.
来清理查询池
省略了很多细节,对于像我这样的 Vulkan 菜鸟来说,这个设置很可怕,花了好几天时间才搞清楚。希望这会让其他人头疼。
更多信息见 documentation。
总结
我希望能够测量 运行 整个图形管道在 GPU 上经过的时间(以毫秒为单位)。目标:为了能够保存基准 before/after 优化代码(下一步将是 mipmapping 纹理)以查看改进。这在 OpenGL 中非常简单,但我是 Vulkan 的新手,需要一些帮助。
我浏览了相关的现有答案 (
通过文档页面,我发现了几个我认为应该使用的函数,所以我准备了这样的东西:
1:正在创建查询池
void CreateQueryPool()
{
VkQueryPoolCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO;
createInfo.pNext = nullptr; // Optional
createInfo.flags = 0; // Reserved for future use, must be 0!
createInfo.queryType = VK_QUERY_TYPE_TIMESTAMP;
createInfo.queryCount = mCommandBuffers.size() * 2; // REVIEW
VkResult result = vkCreateQueryPool(mDevice, &createInfo, nullptr, &mTimeQueryPool);
if (result != VK_SUCCESS)
{
throw std::runtime_error("Failed to create time query pool!");
}
}
我有 queryCount = mCommandBuffers.size() * 2
的想法 space 用于渲染前后的单独查询时间戳,但我不知道这个假设是否正确。
2:记录命令缓冲区
// recording command buffer i:
vkCmdWriteTimestamp(mCommandBuffers[i], VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, mTimeQueryPool, i);
// render pass ...
vkCmdWriteTimestamp(mCommandBuffers[i], VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, mTimeQueryPool, i);
vkCmdCopyQueryPoolResults(/* many parameters here */);
我正在寻找一些说明:
- 写入同一个查询索引的后果是什么?我是否需要两个单独的查询池 - 一个用于渲染时间之前,一个用于渲染时间之后?
- 我应该如何处理同步?我假设每个命令缓冲区都有一个单独的查询。
- 对于包含查询结果的目标缓冲区,存储在“主机可见位”的某个地方是否足够好,或者我是否需要“仅设备可见”的暂存内存?我对这个也有点迷茫。
我无法找到任何关于如何测量渲染时间的在线示例,但我只是假设这是一项如此常见的任务,所以肯定在某处有一个示例。
写入同一个查询索引是不好的,因为您在同一个查询索引处用“之后”时间戳覆盖了“之前”时间戳。您可能希望将写入时间戳调用中的最后一个参数更改为“之前”调用的 i * 2
和“之后”调用的 i * 2 + 1
。您已经为每个命令缓冲区分配了 2 个时间戳,但只使用了其中的一半。该方案最终为每个命令缓冲区 i
.
我没有任何使用经验vkCmdCopyQueryPoolResults()
。如果您可以让您的队列空闲,那么在空闲之后,调用 vkGetQueryPoolResults()
这对于您在这里所做的事情来说可能会容易得多。它将查询结果复制回主机内存,您不必将同步写入另一个缓冲区然后 mapping/reading 返回。
所以,感谢@karlschultz,我设法让一些东西工作了。因此,为了防止其他人寻找相同的答案,我决定在此处 post 我的发现。对于那里的 Vulkan 专家:如果我犯了明显的错误,请告诉我,我会在这里纠正它们!
查询池创建
我按照我的问题中的描述填写了一个 VkQueryPoolCreateInfo
结构,并让它的 queryCount
字段等于命令缓冲区数量的两倍,以便在 [之前存储 space 用于查询=65=]和渲染后。
重要的是在使用查询之前重置查询池中的所有条目,和在写入查询后重置查询。这需要进行一些更改:
1) 询问图形队列是否支持时间戳
选择图形队列家族时,structVkQueueFamilyProperties
有一个字段timestampValidBits
必须大于0,否则队列家族不能用于时间戳查询!
2) 确定时间戳周期
物理设备包含一个特殊值,该值指示时间戳查询递增 1 所需的纳秒数。这对于将查询结果解释为例如纳秒或毫秒。该值是 float
,可以通过调用 vkGetPhysicalDeviceProperties
并查看字段 VkPhysicalDeviceProperties.limits.timestampPeriod
.
3) 请求查询重置支持
在逻辑设备创建过程中,必须填写一个结构并将其添加到pNext
链中以启用主机查询重置功能:
VkDeviceCreateInfo createInfo{};
VkPhysicalDeviceHostQueryResetFeatures resetFeatures;
resetFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_HOST_QUERY_RESET_FEATURES;
resetFeatures.pNext = nullptr;
resetFeatures.hostQueryReset = VK_TRUE;
createInfo.pNext = &resetFeatures;
4) 记录命令缓冲区
时间戳查询应该在渲染过程的范围之外,如下所示。由于管道阶段的(潜在)时间重叠,无法测量单个着色器(例如片段着色器)的 运行 时间,只能测量整个管道或渲染通道范围之外的任何内容。
vkCmdWriteTimestamp(mCommandBuffers[i], VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, mTimeQueryPool, i * 2);
vkCmdBeginRenderPass(/* ... */);
// render here...
vkCmdEndRenderPass(mCommandBuffers[i]);
vkCmdWriteTimestamp(mCommandBuffers[i], VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, mTimeQueryPool, i * 2 + 1);
5) 检索查询结果
我们有两种方法:vkCmdCopyQueryPoolResults
和 vkGetQueryPoolResults
。我选择了后者,因为它大大简化了设置并且不需要与 GPU 缓冲区同步。
鉴于我有一个交换链索引(在我的场景中同样是命令缓冲区索引!),我有这样的设置:
void FetchRenderTimeResults(uint32_t swapchainIndex)
{
uint64_t buffer[2];
VkResult result = vkGetQueryPoolResults(mDevice, mTimeQueryPool, swapchainIndex * 2, 2, sizeof(uint64_t) * 2, buffer, sizeof(uint64_t),
VK_QUERY_RESULT_64_BIT);
if (result == VK_NOT_READY)
{
return;
}
else if (result == VK_SUCCESS)
{
mTimeQueryResults[swapchainIndex] = buffer[1] - buffer[0];
}
else
{
throw std::runtime_error("Failed to receive query results!");
}
// Queries must be reset after each individual use.
vkResetQueryPool(mDevice, mTimeQueryPool, swapchainIndex * 2, 2);
}
变量mTimeQueryResults
指的是std::vector<uint64_t>
,其中包含每个交换链的结果。我使用它通过使用步骤 2 中确定的时间戳周期来计算每秒的平均渲染时间。
并且一定不要忘记通过调用 vkDestroyQueryPool
.
省略了很多细节,对于像我这样的 Vulkan 菜鸟来说,这个设置很可怕,花了好几天时间才搞清楚。希望这会让其他人头疼。
更多信息见 documentation。