将 "preload" 数据存储到计算着色器的共享存储以加快读取访问是否有意义?

Does it make sense to "preload" data to a shared storage of a compute shader for faster read access?

我有以下计算着色器:

#version 450

layout (local_size_x = 128, local_size_y = 1, local_size_z = 1) in;

layout(push_constant) uniform PushConstant
{
    vec2 topLeft;
    vec2 bottomRight;
};

struct Position {
  float x, y, z;
};

layout (set=0, binding=0) buffer PositionBuffer
{
    Position positions[];
};

layout (set=0, binding=1) buffer SelectionBuffer
{
    uint selected[];
};

void main()
{
    uint ind = gl_GlobalInvocationID.z * (gl_WorkGroupSize.x * gl_NumWorkGroups.x) * (gl_WorkGroupSize.y * gl_NumWorkGroups.y)
               + gl_GlobalInvocationID.y * (gl_WorkGroupSize.x * gl_NumWorkGroups.x)
               + gl_GlobalInvocationID.x;

    Position pos = positions[ind];

    selected[ind] = 0;

    if(pos.x > topLeft.x && pos.x < bottomRight.x && pos.y > topLeft.y && pos.y < bottomRight.y)
    {
        selected[ind] = 1;
    }
}

它的作用是检查一个点(来自 positions 缓冲区)是否在用户提供的矩形内(来自 PushConstant)。如果是 - 着色器通过将 1 写入 selected 缓冲区来标记该点。

这段代码工作正常。但是由于我没有计算方面的经验,所以我正在寻找让它变得更好的方法。我知道整个组都可以访问共享变量。这个想法是制作一组共享位置并将其填充到一个线程中,比方说,线程号 0。然后,从理论上讲,其他线程不需要读取缓冲内存,而是读取更快的共享内存。

值得吗?
如何正确同步?
我可以做类似的事情来将数据写入 selected 数组吗?

从你的总操作来看。按顺序,你:

  1. 读取一个连续的内存块。
  2. 对该内存的每个值执行一次操作。
  3. 将该操作的结果写入另一块内存。

您的代码在任何时候都不需要多次读取该值。虽然编写的代码可能会写入两次值,但没有理由 必须 。您可以很容易地根据条件计算出一个值,然后将该值写入内存。我假设一个好的编译器会将您的代码准确地翻译成那样。

因为没有线程同时读取或写入多个位置,所以对内存的缓存访问仅有助于将 "read X bytes" 转变为更高效的 "read cacheline bytes" 读取。尝试从恰好位于同一缓存行中的地址读取的两次调用应该只执行一次内存提取。写作也是如此。写入同一缓存行的多次调用应聚合为一次写入。

当然,这假定了合理的硬件。

这样的系统仍然假设 可能调用同一内存的多个 reads/writes。这与 warp/wavefront 中的调用次数有关(即:以锁步方式执行的着色器的调用次数)。如果每个 warp 读取的数据大小未与缓存对齐,则两个 warp 可能会向同一缓存行发出读取,因为不同的 warp 可能同时执行。写入也是如此。但即便如此,也假设缓存和执行内存获取的决定是在每个 warp 的基础上做出的。

无论如何,如果确定是这种情况,正确的解决方案是尽可能地对齐您的阅读,而不是试图为它做缓存的工作。

有时预缓存数据会很有用,但这主要是在调用频繁从相同地址读取的情况下,通常是在它们从彼此的内存中读取时。即便如此,这也是您应该分析的内容,而不是尝试先验地编写代码。