GLSL:关于连贯限定符

Emi*_*rov 5 opengl synchronization atomic glsl

我没有清楚地了解coherent限定符和原子操作是如何协同工作的。

我使用以下代码在同一个 SSBO 位置执行一些累加操作:

uint prevValue, newValue;
uint readValue = ssbo[index];
do
{
    prevValue = readValue;
    newValue = F(readValue);
}
while((readValue = atomicCompSwap(ssbo[index], prevValue, newValue)) != prevValue);
Run Code Online (Sandbox Code Playgroud)

这段代码对我来说很好用,但是,coherent在这种情况下,我是否需要使用限定符声明 SSBO(或图像)?

我是否需要coherent在我只打电话的情况下使用atomicAdd

我什么时候需要使用coherent限定符?我是否只需要在直接写入的情况下使用它:ssbo[index] = value;

ber*_*nie 8

TL; 博士

我找到了支持关于coherent.

当前分数:

  • 需要coherent原子:1.5
  • 省略coherent原子:5.75

底线,尽管得分仍然不确定。里面一个工作组,我相信大多coherent并不需要在实践中。在这些情况下,我不太确定:

  1. 超过 1 个工作组 glDispatchCompute
  2. 多个glDispatchCompute调用都访问相同的内存位置(以原子方式)glMemoryBarrier,它们之间没有任何调用

但是,coherent当您仅通过原子操作访问它们时,声明 SSBO(或单个结构成员)是否会产生性能成本?根据下面的内容,我不相信是因为在变量的读或写操作中coherent添加了“可见性”指令或指令标志。如果变量只能通过原子操作访问,编译器应该希望:

  1. coherent生成原子指令时忽略,因为它没有效果
  2. 使用适当的机制来确保原子操作的结果在着色器调用、扭曲、工作组或渲染命令之外可见。

OpenGL wiki 的“内存模型”页面

请注意,原子计数器在功能上与原子图像/缓冲区变量操作不同。后者仍然需要连贯的限定词、障碍等。 (于 2020-04-12 移除)

但是,如果内存以不连贯的方式进行了修改,则不能自动保证从该内存中进行的任何后续读取都会看到这些更改。

+1 要求 coherent

从代码英特尔的文章“OpenGL性能提示:原子计数缓存与着色器存储缓冲区对象”

// Fragment shader used bor ACB gets output color from a texture
#version 430 core

uniform sampler2D texUnit;
layout(binding = 0) uniform atomic_uint acb[ s(nCounters) ];
smooth in vec2 texcoord;
layout(location = 0) out vec4 fragColor;

void main()
{
    for (int i=0; i<  s(nCounters) ; ++i) atomicCounterIncrement(acb[i]);
    fragColor = texture(texUnit, texcoord);
}

// Fragment shader used for SSBO gets output color from a texture
#version 430 core

uniform sampler2D texUnit;
smooth in vec2 texcoord;
layout(location = 0) out vec4 fragColor;
layout(std430, binding = 0) buffer ssbo_data
{
    uint v[ s(nCounters) ];
};

void main()
{
    for (int i=0; i< s(nCounters) ; ++i) atomicAdd(v[i], 1);
    fragColor = texture(texUnit, texcoord);
}
Run Code Online (Sandbox Code Playgroud)

请注意,ssbo_data在第二个着色器中没有声明coherent

文章还指出:

出于各种原因,OpenGL 基金会建议在 SSBO 上使用 [atomic counter buffers];然而,改进的性能不是其中之一。这是因为 ACB 在内部实现为 SSBO 原子操作;因此,使用 ACB 并没有真正的性能优势。

所以原子计数器实际上与 SSBO 显然是一回事。(但那些“各种原因”是什么?这些建议在哪里?英特尔是否暗示存在支持原子计数器的阴谋......?)

+1 省略 coherent

GLSL规范

GLSL 规范在描述coherent和原子操作时使用不同的措辞(强调我的):

(4.10) 当使用未声明为一致的变量访问内存时,着色器访问的内存可能会被实现缓存,以便为将来对同一地址的访问提供服务。内存存储可能以这样一种方式缓存,即写入的值可能对访问同一内存的其他着色器调用不可见。该实现可以缓存通过内存读取获取的值,并将相同的值返回给访问相同内存的任何着色器调用,即使底层内存自第一次内存读取后已被修改。

(8.11) 原子内存函数对存储在缓冲区对象或共享变量存储中的单个有符号或无符号整数执行原子操作。所有原子内存操作从 memory 读取一个值,使用下面描述的操作之一计算新值,将新值写入 memory,并返回读取的原始值。由原子操作更新的内存内容保证在读取原始值和写入新值之间的任何着色器调用中不会被任何其他赋值或原子内存函数修改。

本节中的所有内置函数都接受具有限制、一致和易失性内存限定组合的参数,尽管它们没有在原型中列出。原子操作将根据调用参数的内存限定进行操作,而不是根据内置函数的形参内存限定进行操作

因此,一方面原子操作应该直接与存储的内存一起工作(这是否意味着绕过可能的缓存?)。另一方面,似乎内存限定(例如coherent)在原子操作的作用中起作用。

+0.5 要求 coherent

OpenGL规范

OpenGL 4.6 规范在第 7.13.1 节“着色器内存访问顺序”中更详细地说明了这个问题

内置的原子内存事务和原子计数器函数可用于以原子方式读取和写入给定的内存地址。虽然由多个着色器调用发出的内置原子函数以相对于彼此未定义的顺序执行,但这些函数执行内存地址的读取和写入,并保证在读取之间没有其他内存事务将写入底层内存并写。原子允许着色器使用共享全局地址进行互斥或作为计数器等用途。

原子操作的意图显然似乎一直是原子的,而不是依赖于coherent限定符。确实,为什么要在不同的着色器调用之间进行不以某种方式组合的原子操作?从多次调用中递增本地缓存的值并让所有调用最终写入一个完全独立的值是没有意义的。

+1 省略 coherent

OpenGL 规范问题 #14

OpenGL 4.6:原子计数器缓冲区是否需要使用glMemoryBarrier调用才能访问计数器?

我们在 OpenGL|ES 会议上再次讨论了这个问题。根据来自 IHV 的反馈及其原子计数器的实现,我们计划像对待其他资源(如图像原子、图像加载/存储、缓冲区变量等)一样对待它们,因为它们需要应用程序的显式同步。规范将更改为将“原子计数器”添加到枚举其他资源的位置。

所描述的规范更改发生在 OpenGL 4.5 到 4.6 中,但与glMemoryBarrier哪个在单个glDispatchCompute.

没有效果

示例着色器

让我们检查由两个简单着色器生成的程序集,看看在实践中会发生什么。

#version 460
layout(local_size_x = 512) in;

// Non-coherent qualified SSBO
layout(binding=0) restrict buffer Buf { uint count; } buf;

// Coherent qualified SSBO
layout(binding=1) coherent restrict buffer Buf_coherent { uint count; } buf_coherent;

void main()
{
  // First shader with atomics (v1)
  uint read_value1 = atomicAdd(buf.count, 2);
  uint read_value2 = atomicAdd(buf_coherent.count, 4);

  // Second shader with non-atomic add (v2)
  buf.count += 2;
  buf_coherent.count += 4;
}
Run Code Online (Sandbox Code Playgroud)

第二个着色器用于比较coherent原子操作和非原子操作之间限定符的效果。

AMD

AMD 发布了指令集架构 (ISA) 文档,该文档Radeon GPU Analyzer相结合,可以深入了解 GPU 如何实际实现这一点。

着色器 v1 (Vega gfx900)

s_getpc_b64           s[0:1]                   BE801C80
s_mov_b32             s0, s2                   BE800002
s_mov_b64             s[2:3], exec             BE82017E
s_ff1_i32_b64         s4, exec                 BE84117E
s_lshl_b64            s[4:5], 1, s4            8E840481
s_and_b64             s[4:5], s[4:5], exec     86847E04
s_and_saveexec_b64    s[4:5], s[4:5]           BE842004
s_cbranch_execz       label_0010               BF880008
s_load_dwordx4        s[8:11], s[0:1], 0x00    C00A0200 00000000
s_bcnt1_i32_b64       s2, s[2:3]               BE820D02
s_mulk_i32            s2, 0x0002               B7820002
v_mov_b32             v0, s2                   7E000202
s_waitcnt             lgkmcnt(0)               BF8CC07F
buffer_atomic_add     v0, v0, s[8:11], 0       E1080000 80020000
label_0010:
s_mov_b64             exec, s[4:5]             BEFE0104
s_mov_b64             s[2:3], exec             BE82017E
s_ff1_i32_b64         s4, exec                 BE84117E
s_lshl_b64            s[4:5], 1, s4            8E840481
s_and_b64             s[4:5], s[4:5], exec     86847E04
s_and_saveexec_b64    s[4:5], s[4:5]           BE842004
s_cbranch_execz       label_001F               BF880008
s_load_dwordx4        s[8:11], s[0:1], 0x20    C00A0200 00000020
s_bcnt1_i32_b64       s0, s[2:3]               BE800D02
s_mulk_i32            s0, 0x0004               B7800004
v_mov_b32             v0, s0                   7E000200
s_waitcnt             lgkmcnt(0)               BF8CC07F
buffer_atomic_add     v0, v0, s[8:11], 0       E1080000 80020000
label_001F:
s_endpgm                                       BF810000
Run Code Online (Sandbox Code Playgroud)

(不知道为什么这里使用exec掩码和分支......)

我们可以看到两个原子操作(在相干和非相干缓冲区上)在 Radeon GPU Analyzer 的所有支持的架构上产生相同的指令:

buffer_atomic_add     v0, v0, s[8:11], 0       E1080000 80020000
Run Code Online (Sandbox Code Playgroud)

解码此指令显示GLC(全局相干)标志设置为0这意味着对于原子操作:“不返回先前的数据值。波前没有 L1 持久性”。修改着色器以使用返回值会将两个原子指令的GLC标志更改为:“返回先前的数据值。波前没有 L1 持久性”。1

2013年的文件(海岛等)对说明有一个有趣的描述BUFFER_ATOMIC_<op>

缓冲对象原子操作。始终全局一致。

所以在 AMD 硬件上,它似乎coherent对原子操作没有影响。

着色器 v2 (Vega gfx900)

s_getpc_b64           s[0:1]                   BE801C80
s_mov_b32             s0, s2                   BE800002
s_load_dwordx4        s[4:7], s[0:1], 0x00     C00A0100 00000000
s_waitcnt             lgkmcnt(0)               BF8CC07F
buffer_load_dword     v0, v0, s[4:7], 0        E0500000 80010000
s_load_dwordx4        s[0:3], s[0:1], 0x20     C00A0000 00000020
s_waitcnt             vmcnt(0)                 BF8C0F70
v_add_u32             v0, 2, v0                68000082
buffer_store_dword    v0, v0, s[4:7], 0 glc    E0704000 80010000
s_waitcnt             lgkmcnt(0)               BF8CC07F
buffer_load_dword     v0, v0, s[0:3], 0 glc    E0504000 80000000
s_waitcnt             vmcnt(0)                 BF8C0F70
v_add_u32             v0, 4, v0                68000084
buffer_store_dword    v0, v0, s[0:3], 0 glc    E0704000 80000000
s_endpgm                                       BF810000
Run Code Online (Sandbox Code Playgroud)

buffer_load_dword对操作coherent缓冲区使用glc标志和其他人不符合市场预期。

在 AMD 上:+1 表示省略coherent

英伟达

可以通过检查由 返回的 blob 来获取着色器的程序集glGetProgramBinary()。这些指令在NV_gpu_program4NV_gpu_program5NV_gpu_program5_mem_extended 中进行了描述。

着色器 v1

!!NVcp5.0
OPTION NV_internal;
OPTION NV_shader_storage_buffer;
OPTION NV_bindless_texture;
GROUP_SIZE 512;
STORAGE sbo_buf0[] = { program.storage[0] };
STORAGE sbo_buf1[] = { program.storage[1] };
STORAGE sbo_buf2[] = { program.storage[2] };
TEMP R0;
TEMP T;
ATOMB.ADD.U32 R0.x, {2, 0, 0, 0}, sbo_buf0[0];
ATOMB.ADD.U32 R0.x, {4, 0, 0, 0}, sbo_buf1[0];
END
Run Code Online (Sandbox Code Playgroud)

存在与否没有区别coherent

着色器 v2

!!NVcp5.0
OPTION NV_internal;
OPTION NV_shader_storage_buffer;
OPTION NV_bindless_texture;
GROUP_SIZE 512;
STORAGE sbo_buf0[] = { program.storage[0] };
STORAGE sbo_buf1[] = { program.storage[1] };
STORAGE sbo_buf2[] = { program.storage[2] };
TEMP R0;
TEMP T;
LDB.U32 R0.x, sbo_buf0[0];
ADD.U R0.x, R0, {2, 0, 0, 0};
STB.U32 R0, sbo_buf0[0];
LDB.U32.COH R0.x, sbo_buf1[0];
ADD.U R0.x, R0, {4, 0, 0, 0};
STB.U32 R0, sbo_buf1[0];
END
Run Code Online (Sandbox Code Playgroud)

LDB.U32对操作coherent缓冲区使用COH,意思是“制作加载和存储操作使用一致的缓存”修改器。

在 NVIDIA 上:+1 表示省略coherent

SPIR-V(带有 Vulkan 目标)

让我们看看glslang SPIR-V 生成器生成了什么 SPIR-V 代码。

着色器 v1

// Generated with glslangValidator.exe -H --target-env vulkan1.1
// Module Version 10300
// Generated by (magic number): 80008
// Id's are bound by 30

                              Capability Shader
               1:             ExtInstImport  "GLSL.std.450"
                              MemoryModel Logical GLSL450
                              EntryPoint GLCompute 4  "main"
                              ExecutionMode 4 LocalSize 512 1 1
                              Source GLSL 460
                              Name 4  "main"
                              Name 8  "read_value1"
                              Name 9  "Buf"
                              MemberName 9(Buf) 0  "count"
                              Name 11  "buf"
                              Name 20  "read_value2"
                              Name 21  "Buf_coherent"
                              MemberName 21(Buf_coherent) 0  "count"
                              Name 23  "buf_coherent"
                              MemberDecorate 9(Buf) 0 Restrict
                              MemberDecorate 9(Buf) 0 Offset 0
                              Decorate 9(Buf) Block
                              Decorate 11(buf) DescriptorSet 0
                              Decorate 11(buf) Binding 0
                              MemberDecorate 21(Buf_coherent) 0 Coherent
                              MemberDecorate 21(Buf_coherent) 0 Restrict
                              MemberDecorate 21(Buf_coherent) 0 Offset 0
                              Decorate 21(Buf_coherent) Block
                              Decorate 23(buf_coherent) DescriptorSet 0
                              Decorate 23(buf_coherent) Binding 1
                              Decorate 29 BuiltIn WorkgroupSize
               2:             TypeVoid
               3:             TypeFunction 2
               6:             TypeInt 32 0
               7:             TypePointer Function 6(int)
          9(Buf):             TypeStruct 6(int)
              10:             TypePointer StorageBuffer 9(Buf)
         11(buf):     10(ptr) Variable StorageBuffer
              12:             TypeInt 32 1
              13:     12(int) Constant 0
              14:             TypePointer StorageBuffer 6(int)
              16:      6(int) Constant 2
              17:      6(int) Constant 1
              18:      6(int) Constant 0
21(Buf_coherent):             TypeStruct 6(int)
              22:             TypePointer StorageBuffer 21(Buf_coherent)
23(buf_coherent):     22(ptr) Variable StorageBuffer
              25:      6(int) Constant 4
              27:             TypeVector 6(int) 3
              28:      6(int) Constant 512
              29:   27(ivec3) ConstantComposite 28 17 17
         4(main):           2 Function None 3
               5:             Label
  8(read_value1):      7(ptr) Variable Function
 20(read_value2):      7(ptr) Variable Function
              15:     14(ptr) AccessChain 11(buf) 13
              19:      6(int) AtomicIAdd 15 17 18 16
                              Store 8(read_value1) 19
              24:     14(ptr) AccessChain 23(buf_coherent) 13
              26:      6(int) AtomicIAdd 24 17 18 25
                              Store 20(read_value2) 26
                              Return
                              FunctionEnd
Run Code Online (Sandbox Code Playgroud)

buf和之间的唯一区别buf_coherent是后者的装饰MemberDecorate 21(Buf_coherent) 0 Coherent。它们之后的用法是相同的。

添加#pragma use_vulkan_memory_model到着色器会启用Vulkan 内存模型并产生以下(缩写)更改:

                              Capability Shader
+                             Capability VulkanMemoryModelKHR
+                             Extension  "SPV_KHR_vulkan_memory_model"
               1:             ExtInstImport  "GLSL.std.450"
-                             MemoryModel Logical GLSL450
+                             MemoryModel Logical VulkanKHR
                              EntryPoint GLCompute 4  "main"
                              
                              Decorate 11(buf) Binding 0
-                             MemberDecorate 21(Buf_coherent) 0 Coherent
                              MemberDecorate 21(Buf_coherent) 0 Restrict
Run Code Online (Sandbox Code Playgroud)

这意味着......我不太清楚,因为我不精通 Vulkan 的复杂性。我确实在 Vulkan 1.2 规范中找到了“内存模型”附录的这个信息丰富的部分

虽然 GLSL(和传统 SPIR-V)将“一致”修饰应用于变量(出于历史原因),但该模型将每个内存访问指令视为具有可选的隐式可用性/可见性操作。GLSL 到 SPIR-V 编译器应该将一个连贯变量上的所有(非原子)操作映射到此模型中的 Make{Pointer,Texel}{Available}{Visible} 标志。

原子操作隐式具有可用性/可见性操作,并且这些操作的范围取自原子操作的范围。

着色器 v2

(跳过完整输出)

buf和之间的唯一区别buf_coherentMemberDecorate 18(Buf_coherent) 0 Coherent

添加#pragma use_vulkan_memory_model到着色器会启用Vulkan 内存模型并产生以下(缩写)更改:

-                             MemberDecorate 18(Buf_coherent) 0 Coherent

-             23:      6(int) Load 22
-             24:      6(int) IAdd 23 21
-             25:     13(ptr) AccessChain 20(buf_coherent) 11
-                             Store 25 24
+             23:      6(int) Load 22 MakePointerVisibleKHR NonPrivatePointerKHR 24
+             25:      6(int) IAdd 23 21
+             26:     13(ptr) AccessChain 20(buf_coherent) 11
+                             Store 26 25 MakePointerAvailableKHR NonPrivatePointerKHR 24
Run Code Online (Sandbox Code Playgroud)

注意在指令级别而不是变量级别控制操作一致性的添加MakePointerVisibleKHRMakePointerAvailableKHR

+1 省略coherent(也许?)

CUDA

CUDA 工具包文档并行线程执行 ISA 部分包含以下信息:

8.5. 范围

每个强操作都必须指定一个范围,这是一组可以直接与该操作交互并建立内存一致性模型中描述的任何关系的线程。有三个作用域:

表 18. 范围

  • .cta:在与当前线程相同的 CTA 中执行的所有线程的集合。
  • .gpu:当前程序中与当前线程在同一计算设备上执行的所有线程的集合。这还包括由同一计算设备上的主机程序调用的其他内核网格。
  • .sys 当前程序中所有线程的集合,包括主机程序在所有计算设备上调用的所有内核网格,以及构成主机程序本身的所有线程。

请注意,warp 不是范围;CTA 是符合内存一致性模型范围的最小线程集合。

关于 CTA:

协作线程阵列 (CTA) 是一组执行相同内核程序的并发线程。网格是一组独立执行的 CTA。

所以在 GLSL 术语中,CTA == 工作组和网格 ==glDispatchCompute调用。

atom指令说明

9.7.12.4. 并行同步和通信指令:atom

线程到线程通信的原子减少操作。

[...]

可选的 .scope 限定符指定可以直接观察此操作的内存同步效果的线程集,如内存一致性模型中所述。

[...]

如果未指定范围,则使用 .gpu 范围执行原子操作。

所以默认情况下,a 的所有着色器调用glDispatchCompute都会看到原子操作的结果......除非 GLSL 编译器生成使用cta范围的东西,在这种情况下它只会在工作组内部可见。然而,后一种情况对应于sharedGLSL 变量,因此它可能仅用于那些变量而不用于 SSBO 操作。NVIDIA 对这个过程并不是很开放,所以我还没有找到一种确定的方法(也许是glGetProgramBinary)。但是,由于cta映射到工作组和gpu缓冲区(即 SSBO、图像等)的语义,我声明:

+0.5 省略 coherent

经验证据

我编写了一个粒子系统计算着色器,它使用 SSBO 支持的变量作为操作数,atomicAdd()并且它可以工作。的使用coherent是没有必要的,即使有512工作组大小。然而,从没有超过1个工作组。这主要是在 Nvidia GTX 1080 上进行测试的,因此如上所示,NVIDIA 上的原子操作似乎总是至少在工作组内部可见。

+0.25 省略 coherent