llvm Best Practices
Developing for LLVM requires adherence to specific patterns and standards to maintain consistency, performance, and testability across a massive codebase. This guide outlines critical best practices for our team.
1. Code Organization and Structure
1.1 Adhere to LLVM Coding Standards
Consistency is paramount. Follow LLVM's established naming conventions, whitespace rules, and C++17 usage. Deviations require strong justification.
- Naming:
CamelCasefor types,lower_casefor functions/variables. - Whitespace: Use
clang-formatwith LLVM style. - C++ Standard: Stick to C++17 features supported by major toolchains.
❌ BAD: Inconsistent naming and formatting
class my_custom_pass { // Class name should be CamelCase
void RunPass() { // Function name should be lower_case
int temp_var = 0;
}
};
✅ GOOD: Consistent LLVM style
class MyCustomPass {
void runOnFunction(llvm::Function &F) { // LLVM passes typically run on a Function
int tempVar = 0; // Local variables can be camelCase or snake_case, consistency is key.
}
};
1.2 Prefer LLVM ADT and Utilities
Leverage LLVM's specialized data structures and utilities over standard library equivalents for performance and consistency.
- Containers: Use
llvm::SmallVector,llvm::DenseMap,llvm::StringRefetc. - I/O: Use
llvm::raw_ostreamfor all output.
❌ BAD: Using standard library containers and I/O
#include <vector>
#include <map>
#include <iostream>
std::vector<int> myVec;
std::map<std::string, int> myMap;
std::cout << "Debug info: " << val << std::endl;
✅ GOOD: Using LLVM ADT and raw_ostream
#include "llvm/ADT/SmallVector.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/Support/raw_ostream.h"
llvm::SmallVector<int, 8> myVec; // SmallVector avoids heap allocation for small sizes
llvm::DenseMap<llvm::StringRef, int> myMap; // DenseMap is faster for many use cases
llvm::outs() << "Debug info: " << val << "\n"; // Use llvm::outs() for stdout
1.3 Utilize TableGen for Declarative Definitions
For instruction definitions, target descriptions, or pass registration, use TableGen. It reduces boilerplate and ensures consistency.
// In a .td file
def MyInst : Instruction<"my_inst", ...>;
2. Common Patterns and Anti-patterns
2.1 Use LLVM_DEBUG for Debugging
Never use printf or std::cout for debug output. Use LLVM_DEBUG macros, which are compiled out in release builds.
❌ BAD: Debugging with printf or std::cerr
#include <iostream>
void MyPass::runOnFunction(llvm::Function &F) {
std::cerr << "Processing function: " << F.getName().str() << "\n";
// ...
}
✅ GOOD: Debugging with LLVM_DEBUG
#include "llvm/Support/Debug.h"
#define DEBUG_TYPE "my-pass" // Define a debug type for your pass
void MyPass::runOnFunction(llvm::Function &F) {
LLVM_DEBUG(llvm::dbgs() << "Processing function: " << F.getName() << "\n");
// ...
}
2.2 Assertions are Your Friend
Use assert liberally to catch invariants and impossible states early. Always build with assertions enabled during development (-DLLVM_ENABLE_ASSERTIONS=On).
❌ BAD: Relying on runtime crashes for logic errors
if (!Val->hasOneUse()) {
// This should never happen, but no assertion.
}
✅ GOOD: Asserting expected conditions
assert(Val->hasOneUse() && "Expected value to have a single use!");
3. Performance Considerations
3.1 Optimize Data Structure Choices
llvm::SmallVector and llvm::DenseMap are specifically designed for compiler performance. Understand their tradeoffs and use them appropriately.
SmallVector: Avoids heap allocations for small collections.DenseMap/DenseSet: Faster thanstd::map/std::setfor many common cases due to hash-based implementation.
3.2 Incremental Builds
Use CMake effectively for incremental builds. Avoid full rebuilds.
# Configure for debug with assertions
cmake -G Ninja -DLLVM_ENABLE_ASSERTIONS=On -DCMAKE_BUILD_TYPE=Debug ../llvm-project/llvm
# Build only the necessary targets
ninja MyPassName
4. Common Pitfalls and Gotchas
4.1 Forgetting Pre-Commit Tests
Always add a baseline test before your change, then a second commit with the actual change and test diffs. This is crucial for review and regression safety.
4.2 Manual CHECK Line Editing
Never manually edit CHECK lines in LIT tests. Use llvm/utils/update_test_checks.py.
❌ BAD: Manually updating CHECK lines
; CHECK: define i32 @foo() {
; CHECK-NEXT: ret i32 42
; Manually updated this
✅ GOOD: Using the script
llvm/utils/update_test_checks.py --opt-binary build/bin/opt llvm/test/Transforms/InstCombine/my_test.ll
5. Testing Approaches
5.1 Embrace the Test-First Workflow
For new optimizations or bug fixes, always write the test first.
- Commit 1: Add a new LIT test case that demonstrates the original behavior (e.g., the bug or the pre-optimization IR). Use
update_test_checks.pyto generate baselineCHECKlines. - Commit 2: Implement your change. Run
update_test_checks.pyagain to update theCHECKlines to reflect the new, desired behavior.
5.2 Comprehensive LIT Testing
LLVM's LIT framework is the definitive testing tool. Ensure your tests are:
- Minimal: Test only the specific pattern being transformed.
- Meaningful Names: Use descriptive test names (e.g.,
@@add_of_selects_multi_use). - Negative Tests: Include cases where your transform should not apply.
- Multi-Use Tests: Verify your transform doesn't increase instruction count if results are used multiple times.
- Commuted Tests: For commutative operations, test swapped operands (unless one is a constant, which is canonicalized).
- Vector Tests: Include splat, splat-with-poison, and non-splat vector cases.
- Flag Tests: Test interactions with flags like
nuw,nsw, and specific fast-math-flags (nnan,nsz,reassoc).
Example of a multi-use test:
; RUN: opt -S -instcombine < %s | FileCheck %s
; CHECK-LABEL: @add_mul_const_multi_use
; CHECK: %[[ADD:.*]] = add i8 %x, 1
; CHECK: call void @use(i8 %[[ADD]])
; CHECK: %[[MUL:.*]] = mul i8 %[[ADD]], 3
; CHECK: ret i8 %[[MUL]]
define i8 @add_mul_const_multi_use(i8 %x) {
%add = add i8 %x, 1
call void @use(i8 %add) ; Extra use
%mul = mul i8 %add, 3
ret i8 %mul
}
declare void @use(i8)
5.3 Provide Alive2 Proofs
For complex transformations, especially in InstCombine, include Alive2 proofs in your pull request description to demonstrate correctness. This significantly speeds up review.
// Example Alive2 proof snippet in PR description
// %0 = add i32 %a, 1
// %1 = mul i32 %0, 2
// =>
// %1 = add i32 (mul i32 %a, 2), 2