图像过滤操作(例如模糊,SSAO,Bloom等)通常使用像素着色器和“聚集”操作完成,其中每个像素着色器调用都会发出许多纹理提取操作以访问相邻像素值,并计算单个像素结果。这种方法在理论上效率低下,因为完成了许多冗余提取:附近的着色器调用将重新提取许多相同的纹理像素。

另一种方法是使用计算着色器。这些具有潜在的优势,即能够在一组着色器调用之间共享少量内存。例如,您可以让每个调用获取一个纹理元素并将其存储在共享内存中,然后从那里计算结果。

问题是在什么情况下(如果有的话)compute-shader方法实际上比pixel-shader方法更快?它是否取决于内核的大小,它是哪种过滤操作等?显然,答案从一个GPU型号到另一个GPU都不同,但是我很想听听是否存在任何普遍趋势。

评论

如果计算着色器正确完成,我认为答案是“总是”。这并非易事。从概念上讲,对于图像处理算法而言,计算着色器比像素着色器更好地匹配。但是,像素着色器提供的余地较小,可以用来编写性能不佳的滤镜。

@bernie您能否阐明“正确完成”计算着色器需要什么?也许写一个答案?总是很高兴获得关于该主题的更多观点。 :)

现在看看你让我做什么! :)

除了跨线程共享工作之外,使用异步计算的能力也是使用计算着色器的重要原因之一。

#1 楼

用于图像处理的计算着色器的体系结构优势是它们跳过了ROP步骤。即使不使用像素着色器,写入很有可能会通过所有常规混合硬件。一般而言,计算着色器通过不同的(通常是更直接的)内存路径,因此可以避免原本会遇到的瓶颈。我听说这归因于相当大的性能优势。

计算着色器的体系结构缺点是GPU不再知道哪个工作项退回到哪个像素。如果您使用的是像素着色管线,GPU可以将工作打包到一个扭曲/波前中,并写入内存中连续的渲染目标区域(可能是Z阶平铺或类似的表现)原因)。如果您使用的是计算管道,则GPU可能无法再以最佳批次运行,从而导致更多带宽使用。但是,如果您知道自己的特定操作具有一个子结构,则可以通过将相关工作打包到同一线程组中来加以利用。就像您说的那样,理论上您可以通过以下方式给采样硬件一个休息时间:对每个通道采样一个值,并将结果放入成组共享存储器中,以便其他通道无需采样即可访问。这是否要取胜取决于组共享内存的昂贵程度:如果它比最低级别的纹理缓存便宜,那么这也许是一个取胜,但不能保证。 GPU已经可以很好地处理高度局部的纹理提取(必要时)。

如果您在操作中有一个中间阶段想要共享结果,则使用组共享内存可能更有意义(因为您必须先将中间结果实际写到内存中才能使用纹理采样硬件)。不幸的是,您也不能不依赖于任何其他线程组的结果,因此第二阶段必须将其自身限制为仅在同一图块中可用。我认为这里的典型示例是计算屏幕自动曝光的平均亮度。我也可以想象将纹理上采样与其他操作结合起来(因为上采样与下采样和模糊不同,不依赖于给定图块之外的任何值)。

评论


$ \ begingroup $
我严重怀疑如果禁用混合,则ROP会增加性能开销。
$ \ endgroup $
–GroverManheim
16年5月20日在3:33

$ \ begingroup $
@GroverManheim取决于体系结构!即使禁用混合,输出合并/ ROP步骤也必须处理排序保证。对于全屏三角形,没有任何实际订购危险,但硬件可能不知道。硬件中可能有一些特殊的快速路径,但是要确定您有资格获得这些…
$ \ endgroup $
– John Calsbeek
16年5月20日在6:05

#2 楼

John已经写了一个很好的答案,因此请考虑将此答案作为他的扩展。

我目前正在为各种算法使用计算着色器。总的来说,我发现计算着色器比同等的像素着色器或基于变换反馈的替代方法要快得多。在很多情况下都有意义。使用像素着色器过滤图像需要设置帧缓冲区,发送顶点,使用多个着色器阶段等。为什么要过滤图像呢?我认为习惯于渲染全屏四边形进行图像处理无疑是继续使用它们的唯一“有效”理由。我坚信,计算图形领域的新手会发现计算着色器比渲染纹理更自然地适合图像处理。

您的问题特别涉及图像过滤,因此我不会在其他主题上过多地阐述。在我们的某些测试中,仅设置转换反馈或切换帧缓冲区对象以渲染为纹理可能会导致约0.2ms的性能损失。请记住,这不包括任何渲染!在一种情况下,我们将完全相同的算法移植到计算着色器中,并看到了明显的性能提升。

使用计算着色器时,GPU上的更多芯片可以用于完成实际工作。使用像素着色器路线时,所有这些附加步骤都是必需的:


顶点装配(读取顶点属性,顶点除数,类型转换,将其扩展到vec4等) >无论顶点着色器多么小,都需要对其进行调度
光栅化器必须计算一个像素列表以对顶点输出进行着色和内插(可能仅用于图像处理的纹理坐标)
状态(深度测试,alpha测试,剪刀,混合)必须设置和管理

您可能会争辩说,智能驱动程序可以抵消所有前面提到的性能优势。你说的对。这样的驱动程序可以识别出您在不进行深度测试等情况下渲染了全屏四边形,并配置了“快速路径”,跳过了为支持像素着色器所做的所有无用工作。如果某些驱动程序为特定的GPU加速某些AAA游戏中的后处理过程,我不会感到惊讶。如果您不是在玩AAA游戏,您当然可以忘记任何此类处理。

但是,驱动程序无法做的是找到计算着色器管道提供的更好的并行性机会。以高斯滤波器的经典示例为例。使用计算着色器,您可以执行以下操作(是否分离过滤器):


对于每个工作组,将源图像的采样划分为工作组大小并​​存储结果存储到共享内存中。
使用存储在共享内存中的示例结果计算过滤器输出。
写入输出纹理

步骤1是此处的关键。在像素着色器版本中,每个像素对源图像进行多次采样。在计算着色器版本中,每个源纹理像素在工作组中只能读取一次。纹理读取通常使用基于图块的缓存,但是此缓存仍然比共享内存慢得多。

高斯滤波器是较简单的示例之一。其他筛选算法还提供了其他使用共享内存在工作组内共享中间结果的机会。

但是有一个陷阱。计算着色器需要显式的内存屏障来同步其输出。也很少有保护措施来防止错误的内存访问。对于具有良好并行编程知识的程序员,计算着色器提供了更大的灵活性。但是,这种灵活性意味着将计算着色器像普通的C ++代码一样对待并且编写慢速或不正确的代码也变得更加容易。

参考文献


OpenGL Compute Shaders Wiki页面
DirectCompute:优化和最佳实践,Eric Young,NVIDIA Corporation,2010 [pdf]

高效的Compute Shader Proramming,Bill Bilodeau,AMD,2011年? [pps]

用于游戏的DirectCompute-使用计算着色器增强引擎,Layla Mah和Stephan Hodes,AMD,2013年,[pps]

用于AMD GPU的计算着色器优化:并行缩减,Wolfgang Engel,2014年


评论


$ \ begingroup $
您所描述的改进的采样并行性很有趣-我有一个流畅的sim卡,它已经用计算着色器实现了,每个像素有多个实例的许​​多实例。 describe似乎很棒,但我有点挂在嘴上-当相邻像素属于不同的工作组时,如何访问它们?例如,如果我有一个64x64仿真域,分布在numthreads(16,16,1)的dispatch(2,2,1)上,则id.xy == [15,15]的像素将如何获得其相邻像素?
$ \ endgroup $
–折腾
19年11月21日在21:43

$ \ begingroup $
在这种情况下,我看到2个主要选择。 1)将群组大小增加到64以上,并且仅写入64x64像素的结果。 2)首先在64x64工作组中以某种方式对64 + nX64 + n进行了划分,然后使用较大的“输入”网格进行计算。最佳解决方案当然取决于您的特定条件,我建议您写下另一个问题以获取更多信息,因为注释不太适合此问题。
$ \ endgroup $
–伯尼
19年11月23日在17:46

#3 楼

我偶然发现了这个博客:
针对AMD的计算着色器优化

鉴于可以在计算着色器中完成哪些技巧(仅适用于计算着色器),我很好奇是否在计算着色器上进行并行约简比在像素着色器上更快。我给作者沃尔夫·恩格尔发了电子邮件,询问他是否尝试过像素着色器。他回答是,然后再写博客文章时,计算着色器版本明显快于像素着色器版本。他还补充说,今天的差异更大。因此,显然在某些情况下,使用计算着色器可能会具有很大的优势。