I have cleaned up the API in the spirv-tools module I wrote about in a previous blog post. There is currently functionality for
- Reading/writing SPIR-V binaries and my high level assembler
- Iterating over instructions, functions, and basic blocks
- Adding/removing/examining instructions, functions, and basic blocks
- Some optimization passes (dead code elimination, and CFG simplification)
The idea behind the API is that it should correspond directly to the SPIR-V binary; the binary is conceptually represented as
a list of SPIR-V instructions by the
Reading and examining binaries can be done with a minimum of code. For example, here is how to read a SPIR-V binary and count the number of load instructions:
Module
class, and each Instruction
consist of the
operation name, result ID, type ID, and operands. Iterating over the
module returns instructions in the same order as in the binary. The API
also has concepts of functions and basic blocks, and the Function
and BasicBlock
classes encapsulates sub-sequences of the binary's instructions.Reading and examining binaries can be done with a minimum of code. For example, here is how to read a SPIR-V binary and count the number of load instructions:
#!/usr/bin/env python import read_spirv with open('frag.spv', 'r') as stream: module = read_spirv.read_module(stream) nof_loads = 0 for inst in module.instructions(): if inst.op_name == 'OpLoad': nof_loads += 1 print 'Number of load instructions: ' + str(nof_loads)
The main use case for the API is analyzing binaries, test generation, etc., but it is useful for implementing optimizations too. For example, a simple peephole optimization for transforming integer "
-(-x)
" to "x
" can be written as
for inst in module.instructions(): if inst.op_name == 'OpSNegate': op_inst = module.id_to_inst[inst.operands[0]] if op_inst.op_name == 'OpSNegate': src_inst = module.id_to_inst[op_inst.operands[0]] inst.replace_uses_with(src_inst)For each instruction we check if it is an
OpSNegate
instruction, if so, we access the predecessor instruction. If that too is an OpSNegate
, then we replaces all uses of the original instruction with the second instruction's predecessor. This leaves the OpSNegate
dead, so you probably want to run the dead code elimination pass after this, which you do as dead_code_elim.optimize(module)
A slightly more involved example is optimizing integer "
x+x
" to "x<<1
":
for inst in module.instructions(): if (inst.op_name == 'OpIAdd' and inst.operands[0] == inst.operands[1]): const1 = module.get_constant(inst.type_id, 1) sll = ir.Instruction(module, 'OpShiftLeftLogical', module.new_id(), inst.type_id, [inst.operands[0], const1.result_id]) sll.copy_decorations(inst) inst.replace_with(sll)
Here we need to replace the
The
Basic blocks are handled in a similar way to instructions. See the
There are still some functionality missing (see the TODO file) that I'm planning to fix eventually. Please let me know if you need some specific functionality, and I'll bump its priority.
OpIAdd
instruction with a newly created OpShiftLeftLogical
. One complication is that SPIR-V specifies the type partially by the PrecisionLow
, PrecisionMedium
, and PrecisionHigh
decorations,
so we need to copy the decorations each time we create a new
instruction as failing to do this may give a dramatic performance reduction for some architectures. I still think that SPIR-V should change how the type and precision modifiers are handled for graphical shaders...The
module.get_constant
returns an OpConstant
or OpConstantComposite
instruction with the specified type and value. The value should in general have the same number of elements as the type for vector types, but it is allowed to pass a scalar value, which replicates the value over the vector width.inst.replace_with(sll)
replaces inst
with sll
in the basic block, updates all uses of inst
to use sll
, and destroys inst
. It is safe to add/remove instructions while iterating. The only possible issue is that the iterator may not see instructions that are inserted in the current basic block, and you may see an instruction in its old place if it is moved within the current basic block. The predecessors/successors are however always correctly updated — it is only the iteration order that may be surprising.Basic blocks are handled in a similar way to instructions. See the
simplify_cfg
pass for an example of how they are used in reality.There are still some functionality missing (see the TODO file) that I'm planning to fix eventually. Please let me know if you need some specific functionality, and I'll bump its priority.
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.