Spaces:
Sleeping
Sleeping
File size: 3,791 Bytes
463f868 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | **Bytecode Layout**
Current layout: `fixed5x32-v1` uses 5 `i32` words per instruction.
Observed facts from `data/cards_compiled.json`:
- Instructions analyzed: 5,172
- JSON database size: 11,222,487 bytes
- Binary snapshot size: 3,139,615 bytes
- `V` is zero in 36.49% of instructions
- `A_LOW` is zero in 79.39% of instructions
- `A_HIGH` is zero in 79.93% of instructions
- `S` is zero in 45.32% of instructions
- Only 20.07% of instructions need a non-zero high 32 bits of `A`
- Rough zero-bit rate across raw 32-bit words: about 96.75%
- Unique full instructions: 599 out of 5,173, about 11.58%
Instruction shape distribution:
- `op_only`: 1,554 instructions, 30.05%
- `op_v`: 637 instructions, 12.32%
- `op_v_a`: 113 instructions, 2.18%
- `op_v_s`: 1,130 instructions, 21.85%
- `full`: 1,738 instructions, 33.60%
Rough storage estimate:
- Fixed-width words: 25,860
- Compact optional-field words: 13,982
- Potential reduction: 11,878 words, 45.93%
**What This Means**
The fixed-width VM is materially wasteful, but the pain is not just storage. The real coupling is execution math:
- The Rust engine assumes `ip += 5`
- Jumps are encoded in instruction-count units derived from 5-word chunks
- Many helpers use `chunks(5)` directly
- Some higher-level optimizations depend on bytecode offsets lining up with effect indices
So the bytecode is still important, but mostly as a stable execution contract. Its current shape is more of a transport convenience than a semantic necessity.
The zero-bit number is useful, but it overstates the practical gain if read literally. Many instructions are simple and repeated:
- `RETURN` appears everywhere
- common jumps and simple draw/buff effects recur often
- a lot of non-zero fields still only use a handful of bits
So there are really three different optimization levers:
1. Field compaction: stop storing absent `V/A/S` words
2. Narrower payload encoding: stop paying full 32 bits for tiny values
3. Dictionary or template encoding: reuse repeated instructions or repeated short sequences
Field compaction is the safest first step because it preserves the current instruction model.
Template encoding is likely the next-biggest size win because only about 11.6% of full instructions are unique.
**Best Next Step**
Do not jump directly from `fixed5x32-v1` to a fully irregular format.
Safer staged plan:
1. Introduce `BytecodeProgram` and centralize decoding behind an iterator/API.
2. Keep the in-memory interpreter interface stable while abstracting away `chunks(5)`.
3. Add a new layout version that uses a compact header per instruction plus optional payload words.
4. Convert jumps from `ip + 5 + v * 5` math to decoded instruction indices.
5. Keep compiler support for both layouts during migration.
**Recommended Compact Layout**
Use tagged instructions instead of raw 5-word tuples:
- Header word:
- opcode
- flags for `has_v`, `has_a`, `has_s`, `wide_a`
- Followed by only the present payload words
Examples:
- `RETURN` -> `[header]`
- `DRAW 1` -> `[header, v]`
- `SET_TAPPED true @slot` -> `[header, v, s]`
- Filtered effect -> `[header, v, a_low]`
- Wide filter effect -> `[header, v, a_low, a_high, s]`
That keeps decoding simple while removing most zero padding.
**Priority Order**
If the goal is maximum practical win with minimum risk:
1. Prune compiled JSON fields
2. Keep using `cards_compiled.bin`
3. Centralize bytecode access behind an abstraction
4. Only then migrate layout
If the goal is readability and debuggability:
1. Push more common patterns into generated Rust
2. Use bytecode as fallback for uncommon/complex patterns
3. Treat compact bytecode as a second-stage optimization
|