Constant Folding and Dead Code Elimination in spirv-opt

This article examines how the spirv-opt tool optimizes GLSL-derived SPIR-V binaries through constant folding and dead code elimination (DCE). Front-end compilers like glslang typically produce verbose, unoptimized intermediate representation containing redundant calculations and dead branches. By analyzing static single assignment (SSA) definitions, control flow graphs (CFG), and def-use chains, spirv-opt replaces statically determinable operations with fixed values and recursively purges unreachable blocks, unused functions, and orphaned variables to produce lean, high-performance shader binaries.

Generating GLSL-Derived SPIR-V

When a GLSL shader is compiled into SPIR-V via a front-end such as glslangValidator, the compiler preserves language semantics strictly and emits explicit SSA IDs for every intermediate operation. This output often includes literal calculations, redundant scalar conversions, and branch instructions with compile-time known conditions. Because GLSL front-ends prioritize fast emission and compliance over global optimization, the module is left with substantial overhead that spirv-opt is designed to clean up.

Constant Folding Mechanics in spirv-opt

Constant folding identifies operations where all input operands are compile-time constants and computes the results ahead of time, replacing the operation with a direct constant definition.

  1. Def-Use Chain Analysis: spirv-opt maintains an internal DefUseManager that tracks all definitions (SPIR-V instructions producing an ID) and their usages throughout the module. During constant folding passes (such as --fold-spec-const-op-composite or the constant propagation phases in -O), the optimizer checks whether an instruction’s operands resolve back to OpConstant, OpConstantTrue, OpConstantFalse, or OpConstantComposite.
  2. Instruction Evaluation: If the operands are constants, the internal folding engine evaluates the instruction using host-side emulation of SPIR-V semantics. This covers scalar and vector arithmetic (OpFAdd, OpFMul), logical operations (OpLogicalAnd, OpLogicalNot), relational checks (OpFOrdGreaterThan), and composite extraction (OpCompositeExtract).
  3. Instruction Replacement and Propagation: Once evaluated, spirv-opt synthesizes a new constant instruction (or reuses an existing matching one in the global constant table) and updates all references in the def-use chain to point to the new ID. The original instruction is rendered redundant, leaving it eligible for elimination.

Through Conditional Constant Propagation (CCP), spirv-opt pairs this evaluation with branch tracking to resolve values that are constant along specific execution paths, propagating these values across basic blocks.

Dead Code Elimination (DCE)

Dead code elimination strips out components of the SPIR-V module that do not contribute to observable side effects, such as outputs to render targets, storage buffers, or atomic operations. In spirv-opt, DCE operates at multiple granularities:

1. Dead Instruction Elimination

Using the def-use framework, passes like --eliminate-dead-code-aggregates or dead-insert elimination scan instructions within functions. If an instruction produces an SSA value with zero users and the instruction itself possesses no side effects (such as an OpFAdd whose result is never referenced), the instruction is removed from its basic block.

2. Control Flow Graph Simplification and Dead Branch Elimination

Constant folding often simplifies an OpBranchConditional into an effective unconditional jump when the condition resolves to a static true or false.

  • The dead branch elimination pass rewrites the conditional branch into a standard OpBranch targeting only the active block.
  • This leaves the alternative path structurally detached from the CFG.
  • The optimizer runs a dominator tree analysis to identify unreachable basic blocks. Unreachable blocks are stripped of their internal instructions, their merge instructions are resolved, and the blocks themselves are removed from the function structure.

3. Global and Structured DCE

GLSL often introduces struct definitions, global uniform blocks, internal functions, and interface variables that end up unused in the final shader stage. Passes such as --eliminate-dead-functions and --eliminate-dead-members:

  • Build a call graph starting from the entry point (OpEntryPoint). Any function not reachable in the call graph is deleted entirely.
  • Scan module-level declarations (OpVariable) in non-input/output storage classes. If an allocated global variable is never loaded from or stored to, it is removed along with any related decoration instructions (OpDecorate, OpMemberDecorate).
  • Remove orphaned OpType declarations and unused constant definitions left behind after other dead code removal passes have run.

The Optimization Pipeline Loop

Constant folding and dead code elimination operate synergistically. Folding a constant can make a branch static; resolving the branch marks an entire execution path dead; removing that dead path eliminates uses of variables, which in turn permits further dead instruction elimination and exposes new constant values. Consequently, standard optimization pipelines in spirv-opt execute these passes iteratively until a fixed point is reached, yielding a minimal, streamlined SPIR-V module.