I'm writing a shader for rendering the sides of triangles with different colors. I have a value mediump float back = dot(V, N) which is positive if the normal faces away from the camera and negative if towards the camera.

To correctly select which diffuse color to shade a particular fragment, I need something like this:

lowp vec4 color = max(0.0, sign(-back)) * frontdiffuse + max(0.0, sign(back)) * back diffuse;

It's more or less an "unrolled conditional"...

Which code produces faster instructions?

  • x * 0.5 + 0.5, or
  • max(0.0, x) ?

The former may be better mathematically behaved than the latter, but maybe min/max/clamping is super efficient in hardware

  • \$\begingroup\$ This is one thing I like about HLSL in D3D. You can compile the shader into D3D bytecode and then examine the non-native instructions generated to see when the compiler is able to recognize optimization opportunities and when it is not. You can then start to familiarize yourself with the various heuristics the compiler has to use to ensure an optimization does not violate floating-point consistency, for instance. GLSL doesn't have a universal bytecode, so even if you get a hold of the compiled GLSL binary it'd be difficult to analyze the effectiveness of any sort of optimization hints :-\ \$\endgroup\$ – Andon M. Coleman Sep 6 '13 at 8:37
  • \$\begingroup\$ You may want to read this presentation from GDC 2013, it explains the proper thought process for this sort of thing: humus.name/Articles/Persson_LowLevelThinking.pdf \$\endgroup\$ – Andon M. Coleman Sep 6 '13 at 8:38
  • \$\begingroup\$ @AndonM.Coleman OpenGL 4.1 added platform-specific binary shaders. opengl.org/registry/specs/ARB/get_program_binary.txt \$\endgroup\$ – Robert Rouhani Sep 6 '13 at 14:16
  • \$\begingroup\$ @RobertRouhani: Right, but the thing about OpenGL is it does not define a universal bytecode format. In Direct3D, Microsoft implements the one and only HLSL compiler and compiles shaders into D3D bytecode, which drivers then translate into native instructions for the GPU they're driving. GLSL compiled shaders kind of skip the intermediate step that D3D has, and you wind up with instructions that are native to the host GPU - which means even if you could disassemble these shaders, you would have to be familiar with the instruction set of the hardware the shaders were compiled for. \$\endgroup\$ – Andon M. Coleman Sep 6 '13 at 14:28
  • \$\begingroup\$ @AndonM.Coleman Ah, I see what you mean now. Yeah, that would make it easier to check for optimization in general, but NVIDIA describes their ISA in NV_gpu_program4/5, and there are tools like GPU ShaderAnalyzer that can compile GLSL for the most common graphics cards. If you just want to check whether a line of code becomes a MAD instruction, that should suffice. \$\endgroup\$ – Robert Rouhani Sep 6 '13 at 14:47

As you say, the two formulas don't behave the same way mathematically. So if it makes a difference to the visuals, your first priority should be to pick the one that gives you the better-looking result.

Both formulas will be a single instruction on the vast majority of GPUs. x * 0.5 + 0.5 can be done with a mad (multiply-add) instruction, and max is a single instruction as well. So I would expect no performance difference under ordinary circumstances.

However, the sign function isn't so efficient here, as it evaluates three cases (positive, negative, or zero) while you only need two cases (positive or negative). It should be faster to use an if statement or the ? operator to explicitly check whether back is positive or negative and choose the correct color. The shader compiler should turn that into an efficient conditional move (as opposed to a more expensive dynamic branch).

All that being said, this is micro, micro-optimization, and quite possibly premature optimization. :) If your shaders aren't ALU-bound, this may produce no measurable difference in performance whatever. So it's probably not worth worrying about anyway.

| improve this answer | |

step is what you are really looking for. step(0.0, back): = 1.0 if (0 < back), or 0.0 otherwise.

So the lines become:

lowp float aux = step(0.0,back);
lowp vec4 color = (1.0-aux) * frontdiffuse + aux * back_diffuse;

For a more elegant (and maybe optimized) version with mix:

lowp vec4 color = mix(frontdiffuse, back_diffuse, step(0.0,back));
| improve this answer | |

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.