2
\$\begingroup\$

I'm having weird performance problems with a GLSL shader I'm working on. This small example fragment shader:

#version 450
out vec4 fragColor;

struct ST { float a; float b; float c; float d; float e; };

void main() {
    ST s = ST (0.0,0.0,0.0,0.0,0.0);
    ST[1] stack;
    int ptr = 0;
    for (int i = 0; i < 32; i++) {
        if (i > -1) // Always true
            stack[ptr++] = s;
        s = stack[--ptr];
    }

    fragColor = vec4(1.0);
}

has horrible performance, but the same shader with almost anything taken out, for example, removing e from the struct, performs fine. I'm just using these on a fullscreen triangle.

I can't figure out what's going on, and how I could fix this problem in my actual shader. Any ideas?

\$\endgroup\$
  • \$\begingroup\$ Does the problem also happen, if you define the array to have 32 elements when you create it? \$\endgroup\$ – Bálint May 18 at 23:49
  • \$\begingroup\$ Yes, it happens regardless of the array size; however, it doesn't happen if it isn't an array, or if I index into it with a constant. \$\endgroup\$ – Sam May 19 at 0:34
  • \$\begingroup\$ Okay, next idea. A constant makes it better. Does unrolling the loop help the same way (basically doing the same thing, but with hard-coded, constant indices going from 0 to 31)? \$\endgroup\$ – Bálint May 19 at 0:47
  • \$\begingroup\$ Yes, that does work. I wonder if it's trying to prefetch the item at that location, but can't predict it in the first shader. \$\endgroup\$ – Sam May 19 at 2:22
1
\$\begingroup\$

Loops and flow control statements in shaders aren't great. Avoid when possible. At best, they will be slow, and at works, they won't work at all (loops).

GPU cores are optimized for mathematical calculations, not logical flow control of programs (loop exit conditions, if statements etc). When you use such constructs in shaders, it really hurts performance and here is why:

Let's look at a trivial hypothetical situation. Imagine you write a fragment shader with a single if statement in the code. That if statement controls whether or not a calculation is performed. The calculation will be carried out 10% of the time.

What happens when you execute a shader pipeline, is that at the driver/hardware level, a "warp" is executed. Warps are a collection of shader kernels (an instance of a shader/gpgpu program) each of which will run on a single core. After these kernels have executed, the memory they operate opon (in this case, a target buffer texture), will have to be memory fenced and synchronized, and wait until all kernels have exited.

90% of the kernels will finish early, having not had to perform our extra calculation, but the remaining 10% will still be working, thus adding a little extra work some of the time will actually reduce performance all the time.

Then you have code branching. On a cpu, when code branching is executed, difference branches of code are run on different cores, with little to no penalty in performance (compilers sort this out for you), but on gpus this sort of branch prediction is not available, and in most cases will result in performance loss. Loops suffer from this problem, because their exit conditions result in code branching upon compilation.

The solution?

Unroll your loop code, and stick to shaders with a finite predictable scope.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.