opengl - GLSL:关于连贯的限定词

标签 opengl synchronization glsl atomic

我不清楚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;吗?

最佳答案

TL; DR
我发现了支持coherent的两个答案的证据。
当前分数:

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

  • 底线,尽管得分仍然不确定。在一个工作组中,我大都认为实践中不需要coherent。在这些情况下,我不太确定:
  • glDispatchCompute中的1个以上工作组
  • 多个glDispatchCompute调用,所有这些调用都(从原子上)访问相同的内存位置,而它们之间没有任何glMemoryBarrier
    但是,仅通过原子操作访问它们时,声明SSBO(或单个结构成员)coherent是否会降低性能? 基于以下内容,我不相信这是因为coherent在变量的读取或写入操作中添加了“可见性”指令或指令标志。如果仅通过原子操作访问变量,则编译器应希望:
  • 在生成原子指令时忽略coherent,因为它对
  • 没有影响
  • 使用适当的机制来确保原子操作的结果在着色器调用,扭曲,工作组或渲染命令之外可见。

  • OpenGL wiki's "Memory Model" page:

    请注意,原子计数器在功能上与原子图像/缓冲区变量操作不同。后者仍然需要连贯的限定词,障碍等。 (于2020-04-12删除)
    但是,如果以不连贯的方式修改了内存,则不会自动保证从该内存进行的任何后续读取都会看到这些更改。

    +1,要求输入coherent
    Intel's article "OpenGL Performance Tips: Atomic Counter Buffers versus Shader Storage Buffer Objects"中的代码
    // 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)原子存储函数对存储在缓冲区对象或共享变量存储中的单个有符号或无符号整数执行原子操作。所有原子内存操作从内存中读取一个值,使用以下描述的操作之一计算新值,将新值写入内存中,并返回读取的原始值。在读取原始值的时间与写入新值的时间之间,在任何着色器调用中,保证通过原子操作更新的存储器的内容不会被任何其他赋值或原子存储器功能修改。
    尽管未在原型中列出,但本节中的所有内置函数都接受带有限制,一致性和 Volatile 内存限定条件的参数。 原子操作将根据调用自变量的内存限定条件而不是内置函数的形式参数存储限定条件进行操作。

    因此,一方面原子操作应该直接与存储的内存一起工作(这是否意味着绕过了可能的缓存?)。另一方面,似乎内存限定(例如coherent)在原子操作中起作用。
    +0.5,表示需要coherent
    OpenGL规格
    OpenGL 4.6规范在第7.13.1节“着色器内存访问顺序”中更加阐明了此问题。

    内置的原子内存事务和原子计数器功能可用于自动读写给定的内存地址。 虽然由多个着色器调用发出的内置原子函数相对于彼此以未定义的顺序执行,但这些函数执行存储器地址的读和写操作,并确保在存储器之间的其他存储器事务不会写入基础存储器。读写。原子允许着色器将共享的全局地址用于相互排斥或用作计数器,以及其他用途。

    那么,原子操作的意图显然一直都是原子的,而不依赖于coherent限定词。确实,为什么要使用一种原子操作,而该原子操作在不同的着色器调用之间没有以某种方式组合?从多个调用中增加一个本地缓存的值,并让所有这些最终最终写入一个完全独立的值是没有意义的。
    +1,用于省略coherent
    OpenGL规范问题#14
    OpenGL 4.6: Do atomic counter buffers require the use of glMemoryBarrier calls to be able to access the counter?

    我们在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公司
    AMD发布了Instruction Set Architecture (ISA) Documents,再与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
    
    (不知道为什么在这里使用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似乎对原子操作没有影响。
    着色器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
    
    buffer_load_dword缓冲区上的coherent操作使用glc标志,而另一个标志与预期不符。
    在AMD上: +1表示省略了coherent
    英伟达
    通过检查glGetProgramBinary()返回的blob,可以获取着色器的程序集。指令在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
    
    是否存在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
    
    LDB.U32缓冲区上的coherent操作使用COH修饰符,这意味着“使LOAD和STORE操作使用相干缓存”。
    在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
    
    bufbuf_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}标志的相干变量上。
    原子操作暗含可用性/可见性操作,这些操作的范围取自原子操作的范围。

    着色器v2
    (跳过完整输出)bufbuf_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
    
    注意,添加了MakePointerVisibleKHRMakePointerAvailableKHR来控制指令级别而不是变量级别的操作一致性。
    +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调用。
    atom instruction description:

    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

    关于opengl - GLSL:关于连贯的限定词,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56340333/

    相关文章:

    synchronization - OpenMP 命名的关键部分 : if a program variable is used, 是被评估还是被用作字符串而不被评估?

    opengl - 使用光线转换算法进行体积渲染(使用 glsl)

    opengl - glsl 折射被颠倒映射

    mysql - 将 XML 文件同步到 MySQL 数据库

    three.js - 三人组 : compute projected coordinate in fragment shader

    c++ - 通过 Alpha 混合的 OpenGL 掩蔽

    c - opengl View 、投影和正交纵横比

    c++ - 如何在 OpenGL 中设置顶点的不透明度?

    c++ - Mac 中的 OpenGL ARB 函数

    java - 了解Java中的同步块(synchronized block)