-
Notifications
You must be signed in to change notification settings - Fork 624
Description
We've heard reports that SPIR-V modules are larger than those compiled to other representations.
First, SPIR-V binary encoding is extremely regular and is designed to be very simple to handle. It has lots of redundancy. For example, the SPIRV-Tools binary parser is simple and nearly stateless.
Second, Glslang generates binaries with OpName for many objects (see KhronosGroup/glslang#316) Also, it doesn't attempt to use group decorations.
To make smaller binaries, we need to make tools smarter: Emit less redundant info in the first place, make tools to eliminate redundancy (but still produce valid SPIR-V binaries), and make semantically lossless compression and decompression.
This issue is a brain dump of a few ideas along these lines. (Keep in mind that the SPIRV-Tools must remain unencumbered, including a possible relicensing under the Apache 2 license.)
Random ideas include those that leave the result as valid SPIR-V binary:
- Compilers top emitting debug info by default. (E.g. glslang, glslc.) Add option -g to emit the debug info
- Remap Ids, for redundancy removal across many modules (LunarG's spirv-remap)
- Link shaders together into a single SPIR-V module, to share common declarations (like types), and share helper function bodies.
- Write transforms for grouping decorations
- Dead code elimination
- constant folding
- redundant value elimination (global value numbering algorithm)
Generic compression ideas:
- Since SPIR-V is 32-bit word-oriented, use compression algorithms working on a word level. E.g. Huffman encoding of the distinct words in a module.
Low level encoding ideas (stateless):
- Bounded IDs: ID bound tells you how many bits of a 32-bit could be non-zero. Never have to write out the upper bytes if they are always zero. E.g. if you never have IDs more than 255, then write only a single byte for each ID in the module. (Of course, this breaks the word-orientation.)
- Bounded integer constants. Same idea, for positive constants. But doesn’t work well for negative numbers in twos-complement. Fortunately almost all integer literals in a module are unsigned. (Exceptions are the values for OpConstant, OpSpecConstant, and in an OpSwitch, if I remember correctly)
- Use "varint" style encoding of integers as in protocol buffers. It's a nice variable-number-of-bytes encoding of integers that gracefully handles negative numbers using a zig-zag encoding (signed-magnitude, where the sign bit is the LSB). Again, knowledge of the SPIR-V grammar tells you when you have an unsigned value, save a bit by not encoding the sign.
- Lots of instructions have type IDs that can directly be inferred from their arguments, assuming valid SPIR-V. Some, like OpIAdd have a result type that is very restricted, e.g. either the signed or unsigned form of an argument type.
Stateful encoding:
- Instead of writing out an Id value as itself, the encoder uses a dynamically updated table of IDs (mirrored in the decoder) to generate smaller values in the emitted binary. For example an arithmetic instruction will often operate on recently-generated values. So you can use a simple move-to-front heuristic to maintain the table of "most recently mentioned ids". E.g. if the most recently mentioned IDs are %a, %b, %c in that order, then a move-to-front heuristic will put %c in slot 0, %b in slot 1, %a in slot 2. Then to encode %d = OpIAdd %int %a %b then emit the instruction but use 2 for %a and 1 for %b, and suitably update the table. This works well with the varint encodings.
- There are several other variants of the previous idea, not just move-to-front.
- Types IDs can be in their own table, since they have different locality characteristics.
- (Constants might best get separate treatment)
- Don't explicitly write out result ids. Just generate them implicitly. (Requires mirroring of state between encoder and decoder.)
Anyway, this is just a start of what we could do.