Quantifying why blur shaders are so slow (without profiling)
Around a week ago I saw this post on X from the Youtuber Acerola which caught my interest:
View Acerola's post on X
If you have written a blur shader before, you probably know that the kernel for the convolution can be separated into two 1D kernels/passes. In fact, back in my first year of college when I wrote my first blur shader (my second shader ever) I directly implemented a separable version of the Gaussian blur, skipping a naive 2D single pass implementation entirely.
Most resources online will educate you that by separating the kernel, you reduce the number of texture samples needed, therefore making the shader faster. However, have you wondered how a naive implementation is sooo much significantly slower, to the point where you get the unusable result that Acerola encountered? Is nesting another loop really that slow? (TLDR: yes it is because it creates too many texture samples).
The difference here can actually be quantified without any need for fancy profiling tools, just some napkin math.
First let’s consider that the maximum memory bandwidth for the RTX 4090 is ~1 TB/s. This is the fastest that the GPU can possibly go and will be our speed of light reference.
Assuming a RGBA16F texture, one pixel is 8 bytes:
Sampling every pixel in a 40x40 region::
The shader runs on every pixel of the screen, so at a resolution of 1920x1080:
In order to achieve 60 FPS, we would need to read a total of:
This is greater than the maximum bandwidth of the RTX 4090. Getting sufficient performance out of this naive blur is not going to even be physically possible!
By separating the kernel, we only sample a 40x1 region per pixel twice instead of a 40x40 region once. Therefore we would cut down the number of texture reads by a factor of 20, which is obviously a huge difference, resulting in the much more modest ~74.1GiB/s. While blur shaders are a solved problem and we knew this principle ahead of time, this is a good and simple exercise in understanding when you need a fundamental change to your approach of a problem. You wouldn’t want to try to “optimize” the naive 2D implementation by compromising the quality of the blur or making unrelated changes like unrolling a loop.