GLSL Dynamic Branching Penalties in Loops

When a conditional branch inside a GLSL loop depends on per-fragment data, it triggers dynamic branch divergence, one of the most severe performance bottlenecks in GPU rendering. This article details how single-instruction, multiple-threads (SIMT) hardware processes non-uniform conditions, why per-fragment evaluations force thread serialization and idle execution cycles, and how divergent loops amplify the overall computational cost.

The SIMT Architecture and Lockstep Execution

Modern GPUs process fragments in lockstep parallel groups, typically referred to as warps (NVIDIA, 32 threads) or wavefronts (AMD, 32 or 64 threads). All fragment threads within a given warp share a single program counter and execute the exact same instruction at any given clock cycle.

When a branch condition relies on constant data (uniforms) or vertex-derived attributes that evaluate identically across the entire warp, execution remains unified. However, when the condition depends on per-fragment values—such as dynamic textures, dynamic buffers, varying attributes, or computed mathematical results—different fragments in the same warp can evaluate the condition to opposite boolean outcomes.

Dynamic Branch Divergence and Thread Serialization

When adjacent fragments in a warp evaluate a conditional statement differently, the GPU cannot split the warp to run two separate code paths simultaneously. Instead, it must serialize execution using thread masking (predication):

  1. Path A (True): The GPU executes the instructions inside the if block. Threads representing fragments where the condition evaluated to false are masked out (disabled). These threads sit completely idle while drawing power and consuming clock cycles.
  2. Path B (False): The GPU executes the else block (if present). The active/inactive masks are inverted, forcing the threads that took Path A to sit idle while Path B executes.

The execution time for the warp becomes the sum of both paths (\(Time_{True} + Time_{False}\)) rather than just the single branch that an individual fragment needs.

The Multiplying Penalty Inside Loops

Placing a divergent branch inside a loop compounds the overhead with every iteration:

  • Worst-Case Duration: If a branch causes a loop to exit early (break) or skip calculations (continue), the warp cannot stop running until the slowest fragment in the group finishes its final iteration. A single fragment requiring 100 iterations forces all other 31 or 63 fragments to execute 100 iterations of empty, masked cycles.
  • Per-Iteration Desynchronization: If the branch condition fluctuates per iteration based on fragment-specific calculations, threads alternate between being active and inactive repeatedly. This eliminates the benefits of SIMD instruction caching and drastically lowers arithmetic throughput.
  • Instruction Cache Thrashing: Complex diverging branches inside repetitive loops increase the active instruction footprint, reducing instruction cache hits and adding latency stalls while fetching divergent shader code.

Broken Fragment Derivations and Mipmapping

Per-fragment branching also impacts fragment derivatives (dFdx, dFdy). These operations evaluate \(2 \times 2\) pixel quadrands simultaneously to determine surface gradients and mipmap levels. If adjacent fragments within a quad take different execution paths because of dynamic branches, derivative calculations become undefined or explicitly illegal in GLSL, leading to visual rendering artifacts or forced fallback to coarse approximations.

Summary

The fundamental execution penalty for per-fragment dynamic branching in a loop is severe throughput degradation. The warp is serialized to execute every possible execution branch across all iterations, and the fastest fragments are forced to idle at zero computational efficiency until the slowest fragment in the group completes.