GLSL:OpenGL 缓冲区中的结构数组与数组结构

Ran*_*zen 5 arrays performance gpu glsl data-oriented-design

现在,当阅读 Internet 中的不同资源时,如果您要按顺序处理大型数组,那么数组结构似乎是一种非常高效的数据存储方式。

例如在 C++ 中

struct CoordFrames
{
    float* x_pos;
    float* y_pos;
    float* z_pos;
    float* scaleFactor;
    float* x_quat;
    float* y_quat;
    float* z_quat;
    float* w_quat;
};
Run Code Online (Sandbox Code Playgroud)

允许比数组更快地处理大数组(感谢 SIMD)

struct CoordFrame
{
    glm::vec3 position;
    float scaleFactor;
    glm::quat quaternion;
};
Run Code Online (Sandbox Code Playgroud)

GPU 是专为大规模并行计算而设计的处理器。SIMD 是这里的“必备”。所以结论是数组结构在这里最有用。

但 ...

  • 我从未在任何地方看到过这样的 GLSL 着色器(这对我来说是错误的):

    #define NUM_POINT_LIGHTS 16
    uniform float point_light_x[NUM_POINT_LIGHTS];
    uniform float point_light_y[NUM_POINT_LIGHTS];
    uniform float point_light_z[NUM_POINT_LIGHTS];
    uniform float point_light_radius[NUM_POINT_LIGHTS];
    uniform float point_light_color_r[NUM_POINT_LIGHTS];
    uniform float point_light_color_g[NUM_POINT_LIGHTS];
    uniform float point_light_color_b[NUM_POINT_LIGHTS];
    uniform float point_light_power[NUM_POINT_LIGHTS];
    
    Run Code Online (Sandbox Code Playgroud)

    或类似的东西也不经常看到:

    #define NUM_POINT_LIGHTS 16
    uniform vec3 point_light_pos[NUM_POINT_LIGHTS];
    uniform float point_light_radius[NUM_POINT_LIGHTS];
    uniform vec3 point_light_color[NUM_POINT_LIGHTS];
    uniform float point_light_power[NUM_POINT_LIGHTS];
    
    Run Code Online (Sandbox Code Playgroud)

    每个人,包括我,似乎更喜欢这样写 GLSL:

    #define NUM_POINT_LIGHTS 16
    struct PointLight
    {
      vec3 origin;
      float radius;
      vec3 color;
      float power;
    };
    uniform PointLight pointLights[NUM_POINT_LIGHTS];
    
    Run Code Online (Sandbox Code Playgroud)
  • 此外,在阅读有关顶点数组数据的原始 OpenGl Wiki 时,我想知道,突然间,交错数据应该是首选

    作为一般规则,您应该尽可能使用交错属性。

问题:尝试在 GLSL 着色器中使用数组结构有意义吗?

什么是真的?GPU 是否针对我们喜欢编写着色器的方式进行了高度优化,它真的没有任何区别?

小智 3

尽管我目前没有确切的数字,但我认为这总体上没有帮助。

许多现代 GPU 确实使用 SoA 格式。然而,数组部分通常是着色器的多次调用,当查看单个调用时,就好像您在没有 SIMD 的情况下执行一样。因此,特别是对于统一变量,变量的 SoA 布局没有显着的性能差异。

其他一些 GPU 实际上也有 AoS 布局。例如,Intel Sandy Bridge(Core 2011 版)在一个核心上同时执行 2 个顶点着色器,但具有 8 宽 SIMD 单元,本质上是 2 个 vec4 的布局。因此,使用向量可以使编译器更轻松地优化代码。

如果我们看看 SoA 对 CPU 的好处,有两个主要好处:

  • 仅访问具有所需成员的缓存行,从而提高缓存利用率。
  • 能够使用 SIMD 指令轻松加载和存储数据。

更好的缓存利用率对于GPU来说基本是一样的。然而,无论如何,您通常都会针对单个绘制操作优化数据结构,因此您不会遗漏任何成员来提高缓存利用率。例如,尽管在渲染阴影贴图时包含一系列材质作为 AoS 可能仍然很浪费。

使用 SIMD 指令的问题要小得多,因为从单个着色器调用的角度来看,您并没有真正使用 SIMD,因此对加载和存储没有限制。根据架构的不同,可能有一些指令会加载多个元素,但例如使用 AMD GCN 架构,您可以在之后使用单独加载的变量,因此可以加载整个结构并使用它。

我猜想,如果您的计算能力有限,那并不重要,如果您的带宽有限,您应该减小加载数据的大小,您可以使用 SoA 布局来实现该目标。

如果它只是 16 个灯的阵列,我不会担心,因为它非常小,并且可能不会真正使用大量带宽。

至于交错属性,这可能非常依赖于 GPU。例如,对于 Sandy Bridge,通过 2 个顶点着色器调用,您可以通过交错这两个顶点来获得更好的局部性。

然而,在 AMD GCN 上,单个核心可以同时执行 64 个着色器,即使不交错属性,您也可能会获得良好的局部性,因为每个属性都应该填充整个缓存行(假设顶点在以下情况下很接近):您进行索引渲染)。

请记住,GPU、驱动程序和您想要执行的操作之间的性能特征可能会有所不同。对于特定问题来说,没有什么比一个好的基准更好的了。