Vulkan 同步机制 Event Barrier

大家好,接下来将为大家介绍Vulkan 同步机制 Event Barrier 。

接着上一节的内容Vulkan 同步机制 Fence Semaphore,继续介绍Vulkan 同步机制 Event Barrier。

1、Barrier

Barrier是同一个queue中的command,或者同一个subpass中的command所明确指定的依赖关系。barrier的中文释义一般叫栅栏或者屏障,我们可以想象一下有一大串的command乱序执行(实际上可能是顺序开始,乱序结束),barrier就是在中间树立一道栅栏,要求栅栏前后保持一定的顺序。

vkCmdPipelineBarrier(3) API可以用于创建一个Pipeline中的Barrier。注意这个API与fence/semaphore的不同,这个API的前缀是vkCmd,这意味着这是一个向command buffer中记录命令的API:

void vkCmdPipelineBarrier(
    VkCommandBuffer                             commandBuffer,
    VkPipelineStageFlags                        srcStageMask,
    VkPipelineStageFlags                        dstStageMask,
    VkDependencyFlags                           dependencyFlags,
    uint32_t                                    memoryBarrierCount,
    const VkMemoryBarrier*                      pMemoryBarriers,
    uint32_t                                    bufferMemoryBarrierCount,
    const VkBufferMemoryBarrier*                pBufferMemoryBarriers,
    uint32_t                                    imageMemoryBarrierCount,
    const VkImageMemoryBarrier*                 pImageMemoryBarriers);

第一个参数command buffer就是这个命令将要被记录的command buffer。pipe line barrier涉及到两个同步范围,这两个同步范围所处的stage分别由srcStageMaskdstStageMask指明,注意着两个都是mask,所以每一个都可以设置多个阶段。关于各个stage后续有机会我再介绍。dependencyFlags是个比较高级的使用方法,我们这里不多介绍,后续有机会我会开专门的文章来尝试理解。

然后就是由vulkan API一贯味道的三个数组了 ,分别是memoryBarrier bufferMemoryBarrier以及最后的ImageMemoryBarrier。当着三个数组都为空的时候,将会在当前执行环境创建一个Execution Barrier,否则,则创建一个Memory Barrier。

Execution Barrier

Execution Barrier就是简单的执行屏障。我们这里举一个来自于khronos官方的例子。

vkCmdDispatch(...);

vkCmdPipelineBarrier(
    ...
    VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, // srcStageMask
    VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, // dstStageMask
    ...);
    
vkCmdDispatch(...);

两个dispatch中,如果涉及到执行的先后顺序,就需要一个execution barrier。如果没有这个barrier,那么这两个dispatch先后顺序是无法预测的。很可能的情况是,第一个dispatch开始后,第二个dispatch也马上开始。至于谁先结束,无法预测。

Execution Barrier可以有效控制queue中的command或者subpass中的command的执行顺序。

这个示例使用了vulkan中的compute path,毕竟这条路比graphic那条路简单了许多。对于compute path而言,可能的stage只有top, indirect, compute/transfer, bottom。如果是graphic path,可能的stage就多了去了。后续有机会我们会再介绍这个。现在只有理解个大概就可以了。

缺点

Execution Barrier只保证了执行的顺序,对于存储修改的顺序,execution barrier无能为力。

例如,在典型的Read-After-Write的问题中,我们让一个dispatch写入一个resource,随后在下一个dispatch中读取这个resource。如果没有execution barrier,就很可能会出现第一个dispatch正在执行的时候,第二个dispatch也已经开始执行的情况了。这样一来,第二个dispatch所读取的resource的内容就无法保证了。

为了保证这两个dispatch之间的顺序,我们可以在中间插入一个barrier。最开始,我们简单滴插入一个execution barrier。这样一来,只有当第一个dispatch执行完成后,第二个dispatch才能开始执行。execution barrier可以正确保证执行的顺序。然而,这就够了吗?

对于理想模型而言,第一个dispatch执行完毕,更新resource的内容。等到第二个dispatch开始的时候,第二个dispatch可以立即获取resource中更新后的结果。然而,事实上,由于现代GPU同样采取了复杂的缓存控制机制,这个理想的模型是不存在的。一种可能的结果是,第一个dispatch执行完毕后, resource最新的内容被缓存到了某一级cache中。不幸的是,第二个dispatch开始执行的时候,这个cache对第二个dispatch不可见(例如,两个dispatch被分派到了不同的执行单元中)。尽管从顺序上,这的确保证了第一个dispatch先执行,然后才是第二个dispatch,但是我们仍然无法保证第二个dispatch能够看到第一个dispatch更新后的结果。

究其原因,就是因为execution barrier只能保证执行上的顺序,而无法保证对于存储操作的顺序。在多核CPU中我们同样也能看到这样的问题存在,有兴趣的读者可以去翻一下我的专栏,了解多核CPU同步原语的背后思想。为了解决execution barrier无法控制存储的缺点,我们需要引入新的barrier,即memory barrier。

Memory Barrier

memory barrier是一种更严格意义上的barrier。一个memory barrier同时兼备了execution barrier语义。memory barrier的引入主要是为了解决execution barrier中,无法有效控制缓存的缺点。我们当然可以选择让所有的execution barrier天然具有memory barrier语义,这样一来,所有的execution barrier实际上都变成了memory barrier。但是粗暴的设计无法让我们精确地获取所需要的极致性能。

例如,我们有一个Write-After-Read的情况。第一个dispatch先读取一个resource的情况,接下来是第二个dispatch来更新这个resource。如果设计上要求所有的execution barrier都需要具有缓存同步的语义,那么情况就变成了这个样子。第一个dispatch读取resource,然后flush修改到一个合适的部分(可能是GPU全局可见的某一级cache,或者是global memory),然而第二个dispatch才能开始执行。这个额外增加的flush动作,实际上没有flush任何数据,因为第一个dispatch对resource的访问时只读访问,没有修改任何数据。这里,我们只需要一个不带有缓存同步的barrie就可以了。

在Pipeline Barrier API中,可以指定三个数组。这三个数组,分别定义了不同类型的memory barrier:

  • 全局memory barrier
  • buffer上的memory barrier
  • image上的memory barrier

全局memory barrier只有src的访问mask和dst的访问mask,因此作用于当前所有的resource。需要具体操纵某个resource的时候,根据resource的类型,分别使用buffer或者image的memory barrier。

2、Event

Event用于同步提交到同一队列的不同命令,或者同步CPU和队列。它同样也具有两种状态——signaled和unsignaled,与Fence不同的是,它的状态改变既可以在CPU上完成,也可以在GPU上完成,并且它是一种细粒度的同步机制。注意:Event不能用于不同队列的命令之间的同步。

在CPU上,可以调用vkSetEvent来使一个Event变成Signaled的状态;可以调用vkResetEvent来使一个Event变成Unsignaled的状态;可以调用vkGetEventStatus来获取一个Event的当前状态,可以利用这个当前状态来对CPU进行阻塞。

而在GPU上:
1.可以通过vkCmdSetEvent命令来使得一个Event变成Signaled状态,此时该命令附带了一个操作执行同步:根据提交顺序,所有在该命令之前的所有命令都必须在此次把Event设置Signaled状态之前完成。
2.可以通过vkCmdResetEvent命令来使得一个Event变成Unsignaled状态,此时该命令附带了一个操作执行同步:根据提交顺序,所有在该命令之前的所有命令都必须在此次把Event设置Unsignaled状态之前完成。
这里有一点非常需要注意:vkCmdSetEvent和vkCmdResetEvent不能够在一个RenderPass内被执行。

创建一个Event

一个event,基本上和semaphore或者fence一样,由host创建,API为vkCreateEvent(3)

VkResult vkCreateEvent(
    VkDevice                                    device,
    const VkEventCreateInfo*                    pCreateInfo,
    const VkAllocationCallbacks*                pAllocator,
    VkEvent*                                    pEvent);

创建event基本不需要额外的信息,并且在host端使用event也非常简单明了,比较复杂的是如何在device端使用event。

Event支持的操作

device上可以使用vkCmdSetEvent(3)触发(set)一个event,可以使用vkCmdResetEvent(3)重置一个event,还可以使用vkCmdWaitEvents(3)等待一个event被触发。其中,WaitEvents有着和barrier极为类似的设计,可以支持缓存控制。

host上可以使用vkSetEvent(3)触发event,也可以使用vkResetEvent(3)重置一个Event。如果host上需要等待event,需要使用vkGetEventStatus(3)来查询状态。

发布了59 篇原创文章 · 获赞 67 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/u010281924/article/details/105382938
今日推荐