TL;DR
我找到了支持这两个答案的证据coherent
.
目前成绩:
- 要求
coherent
原子:1.5
- 省略
coherent
原子:5.75
底线是,尽管得分仍然不确定。在一个工作组中,我基本上相信coherent
不需要在实践中。在这些情况下我不太确定:
- 超过 1 个工作组
glDispatchCompute
- 多种的
glDispatchCompute
调用全部访问相同的内存位置(原子地),无需任何glMemoryBarrier
它们之间
但是,声明 SSBO(或单个结构成员)是否会产生性能成本coherent
当您仅通过原子操作访问它们时?根据下面的内容,我不相信有,因为coherent
添加“可见性”指令或指令标志在变量的读或写操作处。如果变量仅通过原子操作访问,编译器应该希望:
- ignore
coherent
生成原子指令时,因为它没有效果
- 使用适当的机制来确保原子操作的结果在着色器调用、扭曲、工作组或渲染命令之外可见。
来自OpenGL wiki 的“内存模型”页面:
Note that atomic counters are different functionally from atomic image/buffer variable operations. The latter still need coherent qualifiers, barriers, and the like. (removed on 2020-04-12)
然而,如果记忆被以不连贯的方式修改,any随后从该内存读取的是not自动保证看到这些更改。
+1 表示需要coherent
代码来自Intel 的文章《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);
}
请注意ssbo_data
在第二个着色器中未声明coherent
.
文章还指出:
出于各种原因,OpenGL 基金会建议使用[原子计数器缓冲区]而不是 SSBO;然而,性能的提高并不是其中之一。这是因为 ACB 在内部实现为 SSBO 原子操作;因此,使用 ACB 并没有真正的性能优势。
所以原子计数器实际上与 SSBO 显然是一样的。 (但是这些“各种原因”是什么?这些建议在哪里?英特尔是否暗示有一个支持原子计数器的阴谋......?)
+1 省略coherent
GLSL规范
GLSL 规范在描述时使用不同的措辞coherent
和原子操作(重点是我的):
(4.10) 当使用未声明为一致的变量访问内存时,着色器访问的内存可能会被实现缓存,以服务将来对同一地址的访问。内存存储可以以这样的方式进行缓存:写入的值对于访问同一内存的其他着色器调用可能不可见。该实现可以缓存由内存读取获取的值,并将相同的值返回到访问同一内存的任何着色器调用,即使自第一次内存读取以来底层内存已被修改。
(8.11) 原子内存函数对存储在缓冲区对象或共享变量存储中的单个有符号或无符号整数执行原子操作。所有原子内存操作从内存中读取一个值,使用下述操作之一计算新值,将新值写入内存,并返回读取的原始值。在读取原始值和写入新值之间的任何着色器调用中,保证原子操作更新的内存内容不会被任何其他赋值或原子内存函数修改。
本节中的所有内置函数都接受具有限制、一致和易失性内存限定组合的参数,尽管原型中没有列出它们。原子操作将按照调用参数的内存限定的要求进行操作,而不是按照内置函数的形式参数内存限定的要求进行操作.
因此,一方面原子操作应该直接与存储的内存一起工作(这是否意味着绕过可能的缓存?)。另一方面,似乎记忆资格(例如。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;
}
第二个shader用于比较效果coherent
原子操作和非原子操作之间的限定符。
AMD
AMD 发布指令集架构 (ISA) 文档加上Radeon GPU 分析器深入了解 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
(不知道为什么这里使用执行掩码和分支......)
我们可以看到,两个原子操作(在相干和非相干缓冲区上)在 Radeon GPU 分析器所有支持的架构上都会产生相同的指令:
buffer_atomic_add v0, v0, s[8:11], 0 E1080000 80020000
解码该指令表明GLC
(全局一致)标志设置为0
这对于原子操作意味着:“不返回先前的数据值。跨波前没有 L1 持久性”。修改着色器以使用返回值会更改GLC
的旗帜both原子指令1
这意味着:“返回先前的数据值。跨波前没有 L1 持久性”。
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
The buffer_load_dword
操作于coherent
缓冲区使用glc
标志和另一个不符合预期。
On AMD: +1 省略coherent
NVIDIA
可以通过检查返回的 blob 来获取着色器的组件glGetProgramBinary()
。指令描述于NV_gpu_program4, NV_gpu_program5 and NV_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
与否没有区别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
The LDB.U32
操作于coherent
缓冲区使用COH
修饰符,意思是“使 LOAD 和 STORE 操作使用一致的缓存”。
在英伟达上:+1 省略coherent
SPIR-V(带有 Vulkan 目标)
让我们看看 SPIR-V 代码是由glslangSPIR-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
之间唯一的区别buf
and buf_coherent
是后者的装饰MemberDecorate 21(Buf_coherent) 0 Coherent
。它们之后的用法是相同的。
Adding #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
这意味着......我不太知道,因为我不熟悉 Vulkan 的复杂性。我确实找到了这个Vulkan 1.2 规范中“内存模型”附录的信息部分:
虽然 GLSL(和旧版 SPIR-V)将“一致”装饰应用于变量(由于历史原因),但该模型将每个内存访问指令视为具有可选的隐式可用性/可见性操作。 GLSL 到 SPIR-V 编译器应将连贯变量上的所有(非原子)操作映射到此模型中的 Make{Pointer,Texel}{Available}{Visible} 标志。
原子操作隐式具有可用性/可见性操作,并且这些操作的范围取自原子操作的范围。
着色器 v2
(跳过完整输出)
之间唯一的区别buf
and buf_coherent
又是MemberDecorate 18(Buf_coherent) 0 Coherent
.
Adding #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
注意添加MakePointerVisibleKHR
and MakePointerAvailableKHR
在指令级别而不是变量级别控制操作一致性。
+1 省略coherent
(或许?)
CUDA
The CUDA 工具包文档的并行线程执行 ISA 部分有以下信息:
8.5。范围
每个强操作必须指定一个范围,它是可以直接与该操作交互并建立内存一致性模型中描述的任何关系的线程集。范围分为三个:
表 18. 范围
-
.cta
:与当前线程在同一 CTA 中执行的所有线程的集合。
-
.gpu
:当前程序中与当前线程在同一计算设备上执行的所有线程的集合。这还包括同一计算设备上的主机程序调用的其他内核网格。
-
.sys
当前程序中所有线程的集合,包括所有计算设备上主机程序调用的所有内核网格,以及构成主机程序本身的所有线程。
请注意,扭曲不是范围;而是范围。 CTA 是符合内存一致性模型范围的最小线程集合。
关于 CTA:
协作线程数组(CTA)是一组执行相同内核程序的并发线程。网格是一组独立执行的 CTA。
所以用 GLSL 术语来说,CTA == 工作组和网格 ==glDispatchCompute
call.
The atom指令说明:
9.7.12.4。并行同步和通信指令:atom
用于线程间通信的原子缩减操作。
[...]
可选的 .scope 限定符指定可以直接观察此操作的内存同步效果的线程集,如内存一致性模型中所述。
[...]
如果未指定范围,则使用 .gpu 范围执行原子操作。
所以默认情况下,所有着色器调用glDispatchCompute
会看到原子操作的结果...除非 GLSL 编译器生成使用cta
在这种情况下,它仅在工作组内可见。然而,后一种情况对应于shared
GLSL 变量,因此也许它仅用于这些操作,而不用于 SSBO 操作。 NVIDIA 对这个过程不是很开放,所以我还没有找到一种方法来确定(也许是glGetProgramBinary
)。然而,由于语义cta
映射到工作组并gpu
对于缓冲区(即 SSBO、图像等),我声明:
省略+0.5coherent
经验证据
我编写了一个粒子系统计算着色器,它使用 SSBO 支持的变量作为操作数atomicAdd()
它有效。的用法coherent
即使工作组大小为 512,也没有必要。但是,工作组永远不会超过 1 个。这主要是在 Nvidia GTX 1080 上进行测试的,因此如上所示,NVIDIA 上的原子操作似乎至少在工作组内部始终可见。
省略+0.25coherent