我不清楚coherent
限定词和原子运算如何一起工作。
我使用以下代码在同一SSBO位置上执行一些累加操作:
uint prevValue, newValue;
uint readValue = ssbo[index];
do
{
prevValue = readValue;
newValue = F(readValue);
}
while((readValue = atomicCompSwap(ssbo[index], prevValue, newValue)) != prevValue);
此代码对我来说很好用,但是在这种情况下,我是否仍需要使用coherent
限定符声明SSBO(或图像)?
在仅致电coherent
的情况下,我需要使用atomicAdd
吗?
我到底什么时候需要使用coherent
限定词?我是否仅在直接写作:ssbo[index] = value;
的情况下需要使用它?
答案 0 :(得分:5)
我发现了证据支持关于coherent
的两个答案。
当前分数:
coherent
使用原子数:1.5 coherent
的原子:5.75 底线,尽管得分仍然不确定。在一个工作组中,我主要相信在实践中不需要coherent
。在这些情况下,我不太确定:
glDispatchCompute
中有超过1个工作组glDispatchCompute
调用,所有调用(原子地)访问相同的内存位置,而它们之间没有任何glMemoryBarrier
但是,当您仅通过原子操作访问SSBO(或单个结构成员)coherent
时,会产生性能损失吗?基于以下内容,我不会相信是因为coherent
在变量的读取或写入操作中添加了“可见性”指令或指令标志。如果仅通过原子操作访问变量,则编译器希望:
coherent
,因为它没有效果
请注意,原子计数器在功能上与原子图像/缓冲区变量操作不同。后者仍需要连贯的限定词,障碍等。(于2020-04-12删除)但是,如果以不连贯的方式修改了内存,则任何从该内存中进行的后续读取将不自动保证会看到这些更改。
+1需要coherent
// 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规范在描述coherent
和原子操作(强调我的意思)时使用了不同的措辞:
(4.10)当使用未声明为一致性的变量访问内存时,着色器访问的内存可能由实现缓存,以为将来对同一地址的访问提供服务。可以以某种方式缓存内存,使写入的值可能对访问同一内存的其他着色器调用不可见。该实现可以缓存由内存读取获取的值,并将相同的值返回给访问同一内存的任何着色器调用,即使自第一次读取内存以来已修改了基础内存。
(8.11)原子存储函数对存储在缓冲区对象或共享变量存储中的单个有符号或无符号整数执行原子操作。所有原子内存操作从内存中读取一个值,使用以下描述的操作之一计算新值,将新值写入内存,然后返回原始值读。在读取原始值的时间到写入新值的时间之间,在任何着色器调用中,保证通过原子操作更新的内存内容不会被任何其他分配或原子存储功能修改。
本节中的所有内置函数都接受带有限制,一致性和易失性内存限定条件的参数,尽管原型中未列出这些参数。 原子操作将根据调用参数的存储条件而不是内置函数的形式参数存储条件进行操作。
因此,一方面原子操作应该直接与存储的内存一起工作(是否意味着绕过了可能的缓存?)。另一方面,似乎内存限定(例如coherent
)在原子操作中起着作用。
+0.5,表示需要coherent
OpenGL 4.6规范在第7.13.1节“着色器内存访问顺序”中对此问题提供了更多的解释
内置的原子内存事务和原子计数器功能可用于自动读写给定的内存地址。 虽然由多个着色器调用发出的内置原子函数相对于彼此以未定义的顺序执行,但是这些函数执行存储器地址的读和写操作,并确保没有其他存储器事务将写到基础存储器在读写之间。原子允许着色器将共享的全局地址用于相互排斥或用作计数器,以及其他用途。
那么原子操作的意图显然一直都是原子 ,而不取决于coherent
限定词。确实,为什么要使用一种原子操作,而该原子操作在不同的着色器调用之间没有以某种方式组合?通过多次调用增加本地缓存的值,并让所有这些最终最终写入完全独立的值是没有道理的。
+1表示省略了coherent
我们在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;
}
第二个着色器用于比较coherent
限定符在原子操作和非原子操作之间的效果。
AMD发布了Instruction Set Architecture (ISA) Documents,并与Radeon GPU Analyzer结合使用,以深入了解GPU如何实际实现这一目标。
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
(不知道为什么在这里使用exec掩码和分支...)
我们可以看到,两个原子操作(在相干缓冲区和非相干缓冲区上)在Radeon GPU Analyzer的所有受支持的体系结构上都产生相同的指令:
buffer_atomic_add v0, v0, s[8:11], 0 E1080000 80020000
对该指令进行解码,显示GLC
(全局相干)标志设置为0
,这意味着对于原子操作:“不返回先前的数据值。波前没有L1持久性”。修改着色器以使用返回的值会将两者原子指令的GLC
标志更改为1
,这意味着:“返回了先前的数据值。波前没有L1持久性”。
可追溯到2013年的文件(如海岛等)对BUFFER_ATOMIC_<op>
说明进行了有趣的描述:
缓冲区对象原子操作。始终在全球范围内保持一致。
因此在AMD硬件上,看来coherent
对原子操作没有影响。
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
buffer_load_dword
缓冲区上的coherent
操作使用glc
标志,而另一个标志与预期不符。
在AMD上: +1,省略了coherent
可以通过检查glGetProgramBinary()
返回的blob来获取着色器的程序集。这些说明在NV_gpu_program4,NV_gpu_program5和NV_gpu_program5_mem_extended中进行了描述。
!!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
都没有区别。
!!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
LDB.U32
缓冲区上的coherent
操作使用COH
修饰符,这意味着“使LOAD和STORE操作使用相干缓存”。
在NVIDIA上: +1以省略coherent
让我们看看glslang SPIR-V生成器生成了什么SPIR-V代码。
// 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
和buf_coherent
之间唯一的区别是后者用MemberDecorate 21(Buf_coherent) 0 Coherent
装饰。之后它们的用法是相同的。
将#pragma use_vulkan_memory_model
添加到着色器将启用Vulkan memory model并产生这些(缩写)更改:
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的复杂性。我确实找到了这个informative section of the "Memory Model" appendix in the Vulkan 1.2 spec:
尽管GLSL(和传统SPIR-V)将“一致”修饰应用于变量(出于历史原因),但该模型将每条内存访问指令视为具有可选的隐式可用性/可见性操作。从GLSL到SPIR-V的编译器应该将所有(非原子)操作映射到该模型中的Make {Pointer,Texel} {Available} {Visible}标志的相干变量上。
原子操作隐式具有可用性/可见性操作,并且这些操作的范围取自原子操作的范围。
(跳过完整输出)
buf
和buf_coherent
之间的唯一区别再次是MemberDecorate 18(Buf_coherent) 0 Coherent
。
将#pragma use_vulkan_memory_model
添加到着色器将启用Vulkan memory model并产生这些(缩写)更改:
- 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
和MakePointerAvailableKHR
来控制指令级别而不是变量级别的操作一致性。
+1表示省略了coherent
(也许?)
Parallel Thread Execution ISA section of the CUDA Toolkit documentation具有以下信息:
8.5。范围
每个强大的操作必须指定一个范围,该范围是可以直接与该操作交互并建立内存一致性模型中描述的任何关系的一组线程。有三个范围:
表18.范围
.cta
:与当前线程在同一CTA中执行的所有线程的集合。.gpu
:当前程序中与当前线程在同一计算设备上执行的所有线程的集合。这还包括主机程序在同一计算设备上调用的其他内核网格。.sys
当前程序中所有线程的集合,包括由主机程序在所有计算设备上调用的所有内核网格以及构成主机程序本身的所有线程。请注意,扭曲不是作用域; CTA是符合内存一致性模型范围的最小线程集合。
关于CTA:
合作线程数组(CTA)是执行同一内核程序的一组并发线程。网格是一组独立执行的CTA。
因此,按照GLSL术语,CTA ==工作组和网格== glDispatchCompute
调用。
9.7.12.4。并行同步和通信指令:atom
用于线程间通信的原子减少操作。
[...]
可选的.scope限定符指定一组线程,这些线程可以直接观察此操作的内存同步效果,如“内存一致性模型”中所述。
[...]
如果未指定范围,则使用.gpu范围执行原子操作。
因此,默认情况下,glDispatchCompute
的所有着色器调用都会看到原子操作的结果...除非GLSL编译器生成的东西使用cta
范围,在这种情况下,它只会是在工作组中可见。但是,后一种情况对应于shared
GLSL变量,因此也许仅用于那些变量而不用于SSBO操作。 NVIDIA对这个过程不是很开放,所以我还没有找到一种确定的方法(也许使用glGetProgramBinary
)。但是,由于cta
的语义映射到工作组,而gpu
的语义映射到缓冲区(即SSBO,图像等),因此我声明:
+0.5表示省略了coherent
我写了一个粒子系统计算着色器,它使用SSBO支持的变量作为atomicAdd()
的操作数,并且可以工作。即使工作组大小为512,也不必使用coherent
。但是,工作组的数目永远不超过1个。这主要是在Nvidia GTX 1080上进行了测试,因此,在NVIDIA上进行的原子操作似乎在工作组中至少总是可见的。
+0.25表示省略coherent