写入 GPU 计算缓冲区的超级巨大瓶颈?

Super huge bottleneck writing to GPU Compute Buffer?

我正在 Unity 中制作一个计算着色器,但问题是我的代码有一个巨大的瓶颈,我基本上失去了 1000 倍的性能。

我已经创建了一些示例代码来演示该问题,代码的功能不相关并且没有太大意义。

在启用该行的情况下,我失去了写入计算缓冲区 cBuffer[id].vel += vel; 在着色器代码 中的大量性能,我得到了大约 40fps 与 pCount = (1024 * 256); ~256k (in c# code) 但如果我在着色器中禁用写入缓冲区行,我可以在 > 60fps 下执行 pCount = (1024 * 1024 * 64); ~64m,没问题。我想这是因为不同的线程试图写入同一内​​存并且必须等待其他线程完成,但是有没有什么办法可以更聪明地做到这一点?

Download Unity and Visual Studio Project files(unity 2017.3.0f3)

C# 代码:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class computeScript : MonoBehaviour
{
    public ComputeShader cShader;

    struct Particle
    {
        public Vector2 pos;
        public Vector2 vel;
    }

    ComputeBuffer cBuffer;
    const int pCount = (1024 * 256); // <--- set count
    Particle[] particles = new Particle[pCount];
    int kernelCSMain;

    void Start ()
    {
        kernelCSMain = cShader.FindKernel("CSMain");
        cShader.SetInt("pCount", pCount);

        cBuffer = new ComputeBuffer(pCount, (sizeof(float) * 4), ComputeBufferType.Default);

        for(int i = 0; i < pCount; i++)
        {
            particles[i] = new Particle();
            particles[i].pos = new Vector2();
            particles[i].vel = new Vector2();
        }
            cBuffer.SetData(particles);
    }

    void Update ()
    {
        cShader.SetBuffer(kernelCSMain, "cBuffer", cBuffer);
        cShader.Dispatch(kernelCSMain, pCount / 1024, 1, 1);
    }

    void OnDestroy()
    {
        cBuffer.Release();
    }
}

计算着色器代码:

#pragma kernel CSMain

struct Particle
{
    float2 pos;
    float2 vel;
};

RWStructuredBuffer<Particle> cBuffer;
int pCount;

[numthreads(1024,1,1)]
void CSMain (uint id : SV_DispatchThreadID)
{
    float2 vel;
    for (int i = 0; i < pCount; i++) 
    {
        vel += (cBuffer[id].pos + cBuffer[i].pos);
    }
    cBuffer[id].vel += vel; // <---- this line is the issue
}

问题不出在写上,问题出在死码剔除上

如果我拿走你的代码,不写:

[numthreads(1024,1,1)]
void CSMain (uint id : SV_DispatchThreadID)
{
    float2 vel;
    for (int i = 0; i < pCount; i++) 
    {
        vel += (cBuffer[id].pos + cBuffer[i].pos);
    }
}

编译器将检测到 vel 未在任何地方使用(根据,未写入),因此将删除分配它的代码。 然后自该行:

vel += (cBuffer[id].pos + cBuffer[i].pos);

被移除(因为没有使用 vel),编译器检测到循环内容现在是空的,所以也摆脱了循环。

所以在你的例子中,注释行以一个什么都不做的空着色器结束。

为了演示它,这是使用 fxc 编译着色器的结果:

fxc cs.fx /O3 /Tcs_5_0 /ECSMain

首先启用写入:

cs_5_0
dcl_globalFlags refactoringAllowed
dcl_constantbuffer CB0[1], immediateIndexed
dcl_uav_structured u0, 16
dcl_input vThreadID.x
dcl_temps 2
dcl_thread_group 1024, 1, 1
ld_structured_indexable(structured_buffer, stride=16)
(mixed,mixed,mixed,mixed) r0.xy, vThreadID.x, l(0), u0.xyxx
mov r0.zw, l(0,0,0,0)
mov r1.x, l(0)
loop
  ige r1.y, r1.x, cb0[0].x
  breakc_nz r1.y
  ld_structured_indexable(structured_buffer, stride=16)
  (mixed,mixed,mixed,mixed) r1.yz, r1.x, l(0), u0.xxyx
  add r1.yz, r0.xxyx, r1.yyzy
  add r0.zw, r0.zzzw, r1.yyyz
  iadd r1.x, r1.x, l(1)
endloop
ld_structured_indexable(structured_buffer, stride=16)
(mixed,mixed,mixed,mixed) r0.xy, vThreadID.x, l(8), u0.xyxx
add r0.xy, r0.zwzz, r0.xyxx
store_structured u0.xy, vThreadID.x, l(8), r0.xyxx
ret
// Approximately 15 instruction slots used

现在如果你评论你的写和 运行 相同的编译任务:

cs_5_0
dcl_globalFlags refactoringAllowed
dcl_thread_group 1024, 1, 1
ret
// Approximately 1 instruction slots used

此外,请注意,在您的情况下,您正在 运行 在计算中使用 n^2 算法,您的每个粒子都会相互检查(每 262144 个粒子),您正在执行68719476736 "iterations"(这解释了启用写入后性能的急剧下降)