I have written a deferred renderer that can use either a fragment shader or a compute shader to execute the shading pass. Unfortunately, the compute shader implementation runs slower. I'm trying to understand why.
I believe I understand the proximate cause: memory locality when accessing textures. Somehow the fragment shader's accesses are significantly more coherent than the compute shader's.
To demonstrate that, I removed everything except the shadowmapping code and then changed that to sample randomly. Something like (GLSL pseudocode):
uniform sampler2D tex_shadowmap;
uniform float param;
#ifdef COMPUTE_SHADER
layout(local_size_x=8, local_size_y=4, local_size_z=1) in;
#endif
struct RNG { uint64_t state; uint64_t inc; } _rng;
void rand_seed(ivec2 coord) { /*seed `_rng` with hash of `coord`*/ }
float rand_float() { /*return random float in [0,1]*/ }
void main() {
rand_seed(/*pixel coordinate*/);
vec4 light_coord = /*vertex in scaled/biased light's NDC*/;
vec3 shadowmap_test_pos = light_coord.xyz / light_coord.w;
float rand_shadow = 0.0;
for (int i=0;i<200;++i) {
vec2 coord = fract(mix( shadowmap_test_pos.xy, vec2(rand_float(),rand_float()), param ));
float tap = textureLod(tex_shadowmap,coord,0.0).r;
rand_shadow += clamp(shadowmap_test_pos.z,0.0,1.0)<=tap+0.00001 ? 1.0 : 0.0;
}
vec4 color = vec4(vec3(rand_shadow)/200.0,1.0);
/*[set `color` into output]*/
}
When param
is set to 0
, the shadowmap is sampled at shadowmap_test_pos
, and we get correct hard shadows for the scene. In this case, the shadowmap texture lookup locations are somewhat correlated to the pixel coordinate, so we expect good performance. When param
is set to 1
, we get a completely random texture coordinate vec2(rand_float(),rand_float())
, and so the texture lookups are not at all correlated to the pixel coordinate, and we expect bad performance.
Something very interesting happens when we try some more values for param
and measure the latency of the shading pass with a timer query:
As one can see, when working with completely random coordinates (param
=1, right side), the fragment shader and compute shader have the same performance. However, as the coordinates become less random, whatever the fragment shader is doing that makes it more coherent starts to come into play. When the coordinates are deterministic and correlated to screen position (param
≈0, left side), the fragment shader wins by a factor of 2 (note: the param
=0 case was omitted since the GLSL compiler optimizes out the loop).
What's especially weird is that the fragment shader being faster seems to depend on the texture sample coordinate being correlated to the pixel coordinate. For example, if instead of shadowmap_test_pos.xy
I use vec2(0.5)
as the deterministic coordinate, then the effect disappears, and the two shaders have the same performance for any param
.
Both the sources and compiled codes of these shaders are essentially the same. Apart from some setup and writing the data out (which one expects to vary a bit), the shaders are identical. You can see a diff I made of the PTX disassemblies here. Most of the loop body is taken up with the inlined RNG, but the salient point is that it's the same loop.
Note: tested hardware was NVIDIA GTX 1080 with current (446.14) driver.
My question is basically: what can I do about this? I'm working in 8⨯4 tiles in the compute shader, but who knows what the fragment shader is doing. Yet, I would not expect whatever magical secret shading order the fragment shader does to be so much better that you would get a >2⨯ performance difference when you're running the same actual code. (FWIW I've tried different group sizes, to no real change in the above behavior.)
There are a few general discussions about how the different shaders work, but I haven't found anything that can explain this. And, while in the past driver issues have caused weird behavior, compute shaders have now been in core GL for almost 8 years, and using them for deferred shading is an obvious, arguably common, use-case that I would expect to work well.
What am I missing here?