Bridging the Gap: How LLVM SelectionDAG Turns IR into Machine Code
Learn how LLVM's SelectionDAG transforms high-level IR into target-specific machine instructions through legalization, pattern matching, and DAG combining.
22 Sept 2025, 17:39 UTC

The Instruction Selection Gap
Writing a compiler frontend is relatively straightforward: you turn source code into a high-level Intermediate Representation (IR). However, the leap from LLVM IR—which assumes an infinite number of virtual registers and generic operations—to a physical CPU with a fixed instruction set (ISA) is a massive architectural jump. If you simply mapped every IR instruction to a machine instruction 1:1, you would produce bloated, inefficient code that ignores the actual strengths of the hardware.
The SelectionDAG (Selection Directed Acyclic Graph) is the engine LLVM uses to solve this. It transforms linear IR into a graph of dependencies, allowing the compiler to see "patterns" of operations that can be collapsed into a single, powerful hardware instruction.
Legalization: Handling the Unsupported
Not every CPU can do everything. For example, an LLVM IR i64 (64-bit integer) operation is a problem if you are targeting a 32-bit ARM processor. This is where Legalization comes in.
Legalization is the process of ensuring every node in the DAG is "legal" for the target hardware. If a type or operation is unsupported, LLVM expands it. A 64-bit addition on a 32-bit target is broken down into two 32-bit additions with a carry bit. This ensures that by the time the compiler attempts to pick actual machine instructions, it is only working with primitives the hardware actually understands.
Pattern Matching via TableGen
Once the DAG is legalized, LLVM needs to map these generic nodes to specific machine instructions. Instead of writing thousands of lines of C++ if/else statements for every possible CPU instruction, LLVM uses TableGen.
TableGen allows target developers to define patterns in .td files. For example, a pattern might specify that a load followed by an add can be collapsed into a single load-with-offset instruction if the target CPU supports it. The SelectionDAG selector scans the graph for these patterns, effectively "covering" the DAG with the most efficient machine instructions available.
The DAG Combiner: Final Polish
Before the graph is flattened back into a linear sequence of instructions, the DAG Combiner performs peephole optimizations. This phase looks for redundant operations that only became apparent after legalization. Common optimizations include:
- Constant Folding: Replacing
2 + 3with5. - Algebraic Simplification: Replacing
x * 2withx << 1(a bit shift). - Redundancy Removal: Removing a double-negation of a boolean value.
Practical Example: Visualizing the Transformation
You don't have to guess how your code is being transformed. LLVM provides a built-in flag to dump the state of the DAG during the instruction selection process. This is the primary way to debug why a specific optimization isn't triggering.
Run the following command from your terminal (assuming you have llc installed):
llc -print-dag-isel my_code.ll -o my_code.s- Where to run: Your local development environment or build server.
- Permissions: Standard user permissions for executing the LLVM compiler collection.
- Placeholder:
my_code.llis your LLVM bitcode/assembly file. - Expected Result: The terminal will output a series of DAG dumps showing the "Pre-Legalize", "Post-Legalize", and "Post-Combine" states of the graph.
- Risk: For large functions, the output is massive and can freeze a terminal emulator; it is recommended to pipe the output to a file:
llc -print-dag-isel my_code.ll > dag_dump.txt.
Trade-offs: SelectionDAG vs. GlobalISel
SelectionDAG is powerful, but it is computationally expensive. Because it builds a full graph and performs multiple passes of legalization and combining, it consumes significant memory and CPU time during compilation.
To address this, LLVM introduced GlobalISel. While SelectionDAG is a "bottom-up" approach that builds a graph, GlobalISel is a more linear, "top-down" approach. GlobalISel is significantly faster and is becoming the preferred choice for JIT (Just-In-Time) compilers where compilation speed is as critical as execution speed. However, SelectionDAG remains the gold standard for static compilers where maximum code density and performance are the priority.
Verification and Next Steps
To verify that SelectionDAG is producing the expected machine instructions, compare the -print-dag-isel output with the final assembly generated in the .s file. If you see a sequence of generic instructions where you expected a complex target-specific one, the issue usually lies in the .td pattern definitions for that specific target architecture.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.