SPIRV-Tools Optimizer
Writing optimizer passes for SPIRV-Tools (src/ext/SPIRV-Tools).
Pass Skeleton
Derive from spvtools::opt::Pass (or MemPass for mem2reg-style passes), implement name() and Process().
// source/opt/my_pass.h
#ifndef SOURCE_OPT_MY_PASS_H_
#define SOURCE_OPT_MY_PASS_H_
#include "source/opt/pass.h"
namespace spvtools::opt {
class MyPass : public Pass {
public:
const char* name() const override { return "my-pass"; }
Status Process() override;
IRContext::Analysis GetPreservedAnalyses() override {
return IRContext::kAnalysisDefUse | IRContext::kAnalysisCFG;
}
};
} // namespace spvtools::opt
#endif
// source/opt/my_pass.cpp
#include "source/opt/my_pass.h"
namespace spvtools::opt {
Pass::Status MyPass::Process() {
bool modified = false;
// Iterate functions
for (auto& func : *get_module()) {
for (auto& block : func) {
for (auto& inst : block) {
// transform inst...
modified = true;
}
}
}
return modified ? Status::SuccessWithChange : Status::SuccessWithoutChange;
}
} // namespace spvtools::opt
Rules:
name()must match the--my-passCLI flag used inRegisterPassFromFlag(no leading hyphens).Process()must returnStatus::Failureonly on real errors.- If you modify the module, return
Status::SuccessWithChange; the pass manager invalidates analyses not listed inGetPreservedAnalyses(). - A single pass instance may only run once; internal state does not reset.
MemPass base class
Many load/store elimination passes derive from MemPass instead of Pass:
#include "source/opt/mem_pass.h"
class MyMemPass : public MemPass {
// Inherits helpers: GetPtr(), IsTargetVar(), CollectTargetVars(),
// HasOnlyNamesAndDecorates(), KillAllInsts(), Type2Undef(), etc.
};
Key APIs
Module / IRContext
Module* m = get_module(); // or context()->module()
IRContext* ctx = context();
m->ForEachInst([](Instruction* inst){ /* all insts */ }, true);
ctx->get_def_use_mgr(); // analysis::DefUseManager
ctx->get_type_mgr(); // analysis::TypeManager
ctx->get_constant_mgr(); // analysis::ConstantManager
ctx->get_decoration_mgr(); // analysis::DecorationManager
ctx->cfg(); // CFG
ctx->GetValueNumberTable(); // ValueNumberTable
ctx->GetStructuredCFGAnalysis(); // StructuredCFGAnalysis
ctx->InvalidateAnalyses(IRContext::kAnalysisDefUse | IRContext::kAnalysisCFG);
ctx->IsConsistent(); // debug invariant check
Instruction
spv::Op opcode = inst->opcode();
uint32_t rid = inst->result_id(); // 0 if none
uint32_t tid = inst->type_id(); // 0 if none
// Operands: inst->begin() .. inst->end()
for (auto& op : *inst) {
if (spvIsIdType(op.type)) { uint32_t id = op.words[0]; }
}
uint32_t val = inst->GetSingleWordInOperand(idx);
inst->NumInOperands();
inst->SetResultId(new_id);
inst->SetResultType(new_type_id);
inst->SetInOperand(idx, {new_val});
inst->ToBinary(&words);
// Predicates
inst->IsBranch();
inst->IsBlockTerminator();
inst->IsDecoration();
inst->IsConstant();
inst->IsLoad();
inst->IsNop();
inst->ToNop(); // turns instruction into OpNop
BasicBlock
for (auto& block : func) { // func is a Function&
uint32_t label = block.id();
Instruction* label_inst = block.GetLabelInst();
Instruction* merge = block.GetMergeInst(); // OpSelectionMerge / OpLoopMerge
Instruction* loop_merge = block.GetLoopMergeInst();
bool has_phi = block.HasPhiInstructions();
// Iterate instructions (label included if you use ForEachInst)
for (auto& inst : block) { }
block.ForEachInst([](Instruction* i){ }, true); // true = include debug lines
// Terminator helpers
block.ForEachSuccessorLabels([](uint32_t id){ });
bool is_loop_header = block.IsLoopHeader();
uint32_t merge_id = block.MergeBlockIdIfAny();
uint32_t continue_id = block.ContinueBlockIdIfAny();
Instruction* term = block.terminator();
}
Function
for (auto& func : *get_module()) {
uint32_t func_id = func->DefInst().result_id();
bool is_declaration = func->IsDeclaration();
func->ForEachParam([](Instruction* param){ });
// iterate blocks (func is a Function& from the module loop)
for (auto& block : func) { }
}
Function does not have an IsEntryPoint() method. Check entry points via the module:
bool IsEntryPoint(Function* func, Module* module) {
for (auto& entry : module->entry_points()) {
// OpEntryPoint: operand 0 = execution model, operand 1 = function id
if (entry.GetSingleWordInOperand(1) == func->result_id()) return true;
}
return false;
}
Building Instructions
#include "source/opt/ir_builder.h"
// Insert before an instruction
InstructionBuilder b(context(), insertion_point,
IRContext::kAnalysisInstrToBlockMapping | IRContext::kAnalysisDefUse);
// Or append to end of a block
InstructionBuilder b(context(), parent_block,
IRContext::kAnalysisInstrToBlockMapping | IRContext::kAnalysisDefUse);
Instruction* add = b.AddBinaryOp(type_id, spv::Op::OpIAdd, lhs, rhs);
Instruction* extract = b.AddCompositeExtract(elem_type_id, composite_id, {idx0, idx1});
Instruction* construct = b.AddCompositeConstruct(type_id, {id0, id1});
Instruction* load = b.AddLoad(type_id, ptr_id);
Instruction* store = b.AddStore(ptr_id, value_id);
Instruction* branch = b.AddBranch(target_id);
Instruction* cbranch = b.AddConditionalBranch(cond_id, true_id, false_id);
Instruction* cbranch_with_merge = b.AddConditionalBranch(cond_id, true_id, false_id, merge_id);
Instruction* phi = b.AddPhi(type_id, {val0, block0, val1, block1});
Instruction* unary = b.AddUnaryOp(type_id, spv::Op::OpConvertFToS, operand);
Instruction* nullary = b.AddNullaryOp(type_id, spv::Op::OpGroupAll);
Instruction* select = b.AddSelect(type_id, cond_id, true_id, false_id);
Instruction* access = b.AddAccessChain(ptr_type_id, base_ptr_id, {idx_id0, idx_id1});
Instruction* var = b.AddVariable(ptr_type_id, static_cast<uint32_t>(spv::StorageClass::Function));
InstructionBuilder can only preserve kAnalysisDefUse and kAnalysisInstrToBlockMapping; other analyses must be invalidated/rebuilt explicitly.
Replacing / Killing
// Replace all uses of old_id with new_id
ctx->ReplaceAllUsesWith(old_id, new_id);
// Replace uses only when predicate returns true
ctx->ReplaceAllUsesWithPredicate(old_id, new_id, [](Instruction* user) {
return user->opcode() == spv::Op::OpStore;
});
// Kill an instruction (removes from block, updates def-use)
ctx->KillInst(inst);
// Kill an id and all its uses
ctx->KillDef(id);
// Get the defining instruction for an id
Instruction* def = ctx->get_def_use_mgr()->GetDef(id);
// Get users of an id
ctx->get_def_use_mgr()->ForEachUser(id, [](Instruction* user){ });
uint32_t n = ctx->get_def_use_mgr()->NumUsers(id);
Safe Iteration & Phi Nodes
When deleting instructions while iterating, collect first and kill after:
std::vector<Instruction*> to_kill;
get_module()->ForEachInst([&](Instruction* inst) {
if (inst->IsNop()) to_kill.push_back(inst);
}, false);
for (auto* inst : to_kill) context()->KillInst(inst);
Phi operands arrive as (value_id, parent_block_id) pairs:
if (inst->opcode() == spv::Op::OpPhi) {
for (uint32_t i = 0; i + 1 < inst->NumInOperands(); i += 2) {
uint32_t value = inst->GetSingleWordInOperand(i);
uint32_t parent = inst->GetSingleWordInOperand(i + 1);
}
}
Testing
Tests live in test/opt/. Use PassTest<::testing::Test> fixture.
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "test/opt/pass_fixture.h"
#include "test/opt/pass_utils.h"
#include "source/opt/my_pass.h"
using MyPassTest = PassTest<::testing::Test>;
TEST_F(MyPassTest, Basic) {
const std::string before = R"(
OpCapability Shader
OpMemoryModel Logical GLSL450
%void = OpTypeVoid
%main = OpFunction %void None %void
%entry = OpLabel
OpReturn
OpFunctionEnd
)";
const std::string after = before;
SinglePassRunAndCheck<MyPass>(before, after, /*skip_nop=*/false, /*do_validation=*/false);
}
Fixture helpers:
SinglePassRunAndCheck<PassT>(before, after, skip_nop, do_validation, args...)— exact match.SinglePassRunAndCheck<PassT>(before, after, skip_nop, args...)— overload without validation.SinglePassRunAndMatch<PassT>(original, do_validation, args...)— runs pass, disassembles, then checks with EffceeCHECK:patterns embedded inoriginal. Always skips OpNop. Returnsstd::tuple<std::string, Pass::Status>.SinglePassRunAndFail<PassT>(original, args...)— expectsStatus::Failure, checks error messages with EffceeCHECK:patterns embedded inoriginal.SinglePassRunToBinary<PassT>(assembly, skip_nop, args...)— returnsstd::tuple<std::vector<uint32_t>, Pass::Status>.SetAssembleOptions(SPV_TEXT_TO_BINARY_OPTION_PRESERVE_NUMERIC_IDS)— keep ids from assembly.SetDisassembleOptions(SPV_BINARY_TO_TEXT_OPTION_NO_HEADER)— omit SPIR-V header in output.SetTargetEnv(spv_target_env)— change target environment (defaultSPV_ENV_UNIVERSAL_1_3).
For match tests, embed CHECK: lines as comments in the assembly string:
const std::string assembly = R"(
; CHECK: OpReturn
OpCapability Shader
OpMemoryModel Logical GLSL450
%void = OpTypeVoid
%main = OpFunction %void None %void
%entry = OpLabel
OpReturn
OpFunctionEnd
)";
SinglePassRunAndMatch<MyPass>(assembly, false);
Multi-pass tests
TEST_F(MyPassTest, Pipeline) {
const std::string before = R"(... )";
const std::string after = R"(... )";
AddPass<MyFirstPass>();
AddPass<MySecondPass>();
RunAndCheck(before, after);
}
Manual context tests
#include "source/opt/build_module.h"
TEST(MyPass, Manual) {
std::unique_ptr<IRContext> ctx =
BuildModule(SPV_ENV_UNIVERSAL_1_3, nullptr, assembly,
SPV_TEXT_TO_BINARY_OPTION_PRESERVE_NUMERIC_IDS);
ASSERT_NE(ctx, nullptr);
MyPass pass;
auto status = pass.Run(ctx.get());
EXPECT_EQ(status, Pass::Status::SuccessWithChange);
EXPECT_TRUE(ctx->IsConsistent());
}
Registering a Pass
- Add header to
source/opt/passes.h(or include directly). - Add
CreateMyPassPass()factory declaration toinclude/spirv-tools/optimizer.hpp:
Optimizer::PassToken CreateMyPassPass();
- Implement factory in
source/opt/optimizer.cpp:
Optimizer::PassToken CreateMyPassPass() {
return MakeUnique<Optimizer::PassToken::Impl>(MakeUnique<opt::MyPass>());
}
- Add CLI flag mapping in
source/opt/optimizer.cppinsideOptimizer::RegisterPassFromFlag:
} else if (pass_name == "my-pass") {
RegisterPass(CreateMyPassPass());
- Add source files to
source/opt/CMakeLists.txt:
my_pass.h
...
my_pass.cpp
- Add a test target/file under
test/opt/(e.g.my_pass_test.cpp) and list it intest/opt/CMakeLists.txt.
Look at nearby passes in RegisterPassFromFlag for the exact pattern. Passes with arguments parse pass_args before calling RegisterPass(...).
Analyses & Invalidation
Available IRContext::Analysis bits:
kAnalysisNone,kAnalysisDefUse,kAnalysisInstrToBlockMapping,kAnalysisDecorationskAnalysisCombinatorskAnalysisCFG,kAnalysisDominatorAnalysis,kAnalysisLoopAnalysiskAnalysisNameMap,kAnalysisScalarEvolution,kAnalysisRegisterPressurekAnalysisValueNumberTable,kAnalysisStructuredCFG,kAnalysisBuiltinVarIdkAnalysisIdToFuncMapping,kAnalysisConstants,kAnalysisTypeskAnalysisDebugInfo,kAnalysisLiveness,kAnalysisIdToGraphMapping
After Process() returns SuccessWithChange, the pass manager automatically calls:
ctx->InvalidateAnalysesExceptFor(GetPreservedAnalyses());
If you mutate IDs outside normal helpers (e.g. CompactIdsPass), manually invalidate kAnalysisDebugInfo and any others that become stale mid-pass.
If you need an analysis inside Process() and are not preserving it, it is usually fine to request it via ctx->get_def_use_mgr() etc.; the manager will build it on demand. Just make sure GetPreservedAnalyses() reflects what survives your transformations.
Pass Manager / Recipes
spvtools::Optimizer opt(SPV_ENV_UNIVERSAL_1_3);
opt.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&, const char* msg) {
std::cerr << msg << std::endl;
});
opt.RegisterPass(spvtools::CreateCompactIdsPass())
.RegisterPass(spvtools::CreateAggressiveDCEPass());
opt.Run(binary.data(), binary.size(), &optimized);
Built-in recipes:
RegisterPerformancePasses()/RegisterSizePasses()/RegisterLegalizationPasses()- All three also have overloads taking a
bool preserve_interfaceargument.
File Map
| File | Purpose |
|---|---|
source/opt/pass.h / pass.cpp |
Base Pass class |
source/opt/mem_pass.h / mem_pass.cpp |
MemPass base for mem2reg-style passes |
source/opt/empty_pass.h / null_pass.h |
No-op passes for testing |
source/opt/ir_context.h / ir_context.cpp |
IRContext, analysis management |
source/opt/module.h |
Module, header, section lists |
source/opt/function.h |
Function |
source/opt/basic_block.h |
BasicBlock |
source/opt/instruction.h |
Instruction, Operand, DebugScope |
source/opt/ir_builder.h |
InstructionBuilder |
source/opt/def_use_manager.h |
DefUseManager |
source/opt/type_manager.h |
TypeManager |
source/opt/constants.h |
ConstantManager |
source/opt/cfg.h |
CFG |
source/opt/fold.h |
Folder (constant folding) |
source/opt/passes.h |
Unified include for all pass headers |
test/opt/pass_fixture.h |
PassTest fixture |
include/spirv-tools/optimizer.hpp |
Public C++ API |
source/opt/optimizer.cpp |
Pass factories & CLI flag dispatch |
examples/cpp-interface/main.cpp |
Example of assemble / validate / optimize / disassemble |