perf(bit-buffer): fix pre-sync per-code-block allocation in track! (re-target of #194 to master)#197
Conversation
…ind_bit `_buffer_find_bit` assigns the local `sync` in two branches (soft vs. hard detector) and then captures it in the trailing `reduce` closure via `Int(sync.polarity)`. Julia boxes any variable that is both assigned in more than one place and captured by a closure, so this produced a per-call `Core.Box` plus heap-boxing of each `SyncResult` (~80 B). The box exists regardless of runtime path, so it fired on every code block during acquisition, making `track!` allocate in proportion to the signal length (352 B at 1 code period → 1072 B at 10) rather than staying allocation-free after warmup as the docstring claims. Hoist `sync.polarity` into a plain `Int` local before the closure so the closure captures the `Int` and `sync` stays unboxed. Acquisition is now per-code-block allocation-free (flat 272 B regardless of length); the post-sync locked steady state was already clean. No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sions Adds a testset that tracks noise (which never bit-syncs, so the per-code-block bit-edge search `_buffer_find_bit` runs on every block) and asserts `track!` is allocation-free after warmup at both a short and a long chunk length. The existing per-stage allocation test misses this: it tracks a real signal that reaches bit sync within the warmup, so it only ever measures the post-sync path. With the closure-capture box this branch removes, the pre-sync search allocated ~80 B per code block, so the test fails on the old implementation (160 B at 2 blocks, 1600 B at 20 — the 20-block assertion catches the scaling) and passes now. Gated to Julia >= 1.11: on 1.10 the compiler leaves a ~770 B/block allocation in the pre-sync soft-bit path (unrelated to the box) that 1.11+ elides, so the `== 0` assertion only holds on 1.11+. The regression still shows on 1.11+ and in the benchmark suite's memory table. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cf1fbc8 to
8ffedce
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #197 +/- ##
==========================================
- Coverage 98.10% 97.63% -0.48%
==========================================
Files 31 32 +1
Lines 2585 3296 +711
==========================================
+ Hits 2536 3218 +682
- Misses 49 78 +29 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Benchmark Results (minimum time) — macos-14Reporting the minimum over all samples (robust to shared-runner contention), not the median. Alternative backends vs Float32 (
|
| Scenario | F32 | I16 | 1b | 2b | ×I16 | ×1b | ×2b |
|---|---|---|---|---|---|---|---|
| 4-antenna @ 5 MHz | 12.3 μs | 6.22 μs | 4.27 μs | 8.96 μs | 1.97 ✅ | 2.88 ✅ | 1.37 ✅ |
| GPS L1CA, 8 sats @ 40 MHz | 394.0 μs | 125.0 μs | 64.1 μs | 121.0 μs | 3.16 ✅ | 6.14 ✅ | 3.26 ✅ |
| GPS L1CA, 8 sats @ 5 MHz | 56.5 μs | 23.0 μs | 16.8 μs | 25.3 μs | 2.45 ✅ | 3.37 ✅ | 2.23 ✅ |
| Galileo E1B, 4 sats @ 25 MHz | 151.0 μs | 62.8 μs | 2.41 ✅ | ||||
| dynamic taps @ 5 MHz (kernel) | 7.16 μs | 2.38 μs | 1.56 μs | 2.64 μs | 3.01 ✅ | 4.58 ✅ | 2.71 ✅ |
| multi-signal N=3 @ 5 MHz | 11.2 μs | 5.11 μs | 3.86 μs | 6.3 μs | 2.19 ✅ | 2.9 ✅ | 1.78 ✅ |
Time benchmarks (base vs PR head)
Ratio = 0640d0e… / 8ffedce…: >1 means the PR is faster. ✅ ≥ 5 % faster,
| 0640d0e… | 8ffedce… | 0640d0e… / 8ffedce… | |
|---|---|---|---|
| downconvert and correlate/CPU/Float32 | 2.72 μs | 2.66 μs | 1.02 |
| downconvert and correlate/CPU/Float32 4ant | 5.22 μs | 5.22 μs | 1.0 |
| downconvert and correlate/CPU/Float64 | 2.9 μs | 2.88 μs | 1.01 |
| downconvert and correlate/CPU/Int16 | 2.82 μs | 2.8 μs | 1.01 |
| downconvert and correlate/CPU/Int16 4ant | 5.17 μs | 5.15 μs | 1.0 |
| downconvert and correlate/CPU/Int32 | 2.75 μs | 2.71 μs | 1.01 |
| fused kernel/1-ant dynamic taps | 2.74 μs | 2.74 μs | 1.0 |
| fused kernel/1-ant static taps | 2.2 μs | 2.21 μs | 0.998 |
| fused kernel/4-ant dynamic taps | 8.39 μs | 8.33 μs | 1.01 |
| fused kernel/4-ant static taps | 4.67 μs | 4.76 μs | 0.981 |
| fused tuple kernel/1-ant N=2 | 3.51 μs | 3.51 μs | 1.0 |
| fused tuple kernel/1-ant N=3 | 3.65 μs | 3.65 μs | 1.0 |
| fused tuple kernel/2-ant N=2 | 6.76 μs | 6.76 μs | 1.0 |
| fused tuple kernel/2-ant N=3 | 6.02 μs | 6.02 μs | 1.0 |
| fused tuple kernel/4-ant N=2 | 12.4 μs | 12.4 μs | 1.0 |
| fused tuple kernel/4-ant N=3 | 10.8 μs | 10.8 μs | 1.0 |
| track/1. Float32/2K – track | 2.85 μs | 2.83 μs | 1.01 |
| track/1. Float32/2K – track! | 2.96 μs | 2.91 μs | 1.02 |
| track/2. L1 8sat/5K – track | 53.2 μs | 52.9 μs | 1.01 |
| track/2. L1 8sat/5K – track! | 53.1 μs | 52.5 μs | 1.01 |
| track/2. L1 8sat/5K – track! Int16 | 22.1 μs | 22.8 μs | 0.971 |
| track/2. L1 8sat/5K – track! OneBit | 15.9 μs | 16.9 μs | 0.941 |
| track/2. L1 8sat/5K – track!-threaded | 53.2 μs | 53.0 μs | 1.0 |
| track/2. L1 8sat/5K – track-threaded | 53.5 μs | 53.4 μs | 1.0 |
| track/3. E1B 4sat/25K – track | 146.0 μs | 146.0 μs | 1.0 |
| track/3. E1B 4sat/25K – track! | 145.0 μs | 145.0 μs | 1.0 |
| track/3. E1B 4sat/25K – track! Int16 | 62.4 μs | 62.9 μs | 0.991 |
| track/3. E1B 4sat/25K – track!-threaded | 145.0 μs | 145.0 μs | 1.0 |
| track/3. E1B 4sat/25K – track-threaded | 146.0 μs | 146.0 μs | 1.0 |
| track/4. 8L1+8E1B/25K – track | 525.0 μs | 525.0 μs | 1.0 |
| track/4. 8L1+8E1B/25K – track! | 524.0 μs | 524.0 μs | 1.0 |
| track/4. 8L1+8E1B/25K – track!-threaded | 524.0 μs | 524.0 μs | 1.0 |
| track/4. 8L1+8E1B/25K – track-threaded | 525.0 μs | 525.0 μs | 1.0 |
| track/5. multi-signal N=1/5K – track | 6.93 μs | 6.88 μs | 1.01 |
| track/5. multi-signal N=1/5K – track! | 6.92 μs | 6.83 μs | 1.01 |
| track/6. multi-signal N=2/5K – track | 11.3 μs | 11.1 μs | 1.02 |
| track/6. multi-signal N=2/5K – track! | 11.3 μs | 11.2 μs | 1.01 |
| track/7. multi-signal N=3/5K – track | 12.6 μs | 12.4 μs | 1.02 |
| track/7. multi-signal N=3/5K – track! | 12.7 μs | 12.5 μs | 1.02 |
| track/8. L1CA presync 2 blk – track! | 13.4 μs | 13.3 μs | 1.01 |
| track/8. L1CA presync 20 blk – track! | 130.0 μs | 129.0 μs | 1.01 |
| track/8. L1CA synced 2 blk – track! | 13.2 μs | 13.2 μs | 1.0 |
| track/8. L1CA synced 20 blk – track! | 127.0 μs | 127.0 μs | 1.0 |
| time_to_load | 198.0 μs | 209.0 μs | 0.945 |
Memory benchmarks (base vs PR head)
| 0640d0e… | 8ffedce… | |
|---|---|---|
| downconvert and correlate/CPU/Float32 | 2 allocs: 576 B | 2 allocs: 576 B |
| downconvert and correlate/CPU/Float32 4ant | 2 allocs: 848 B | 2 allocs: 848 B |
| downconvert and correlate/CPU/Float64 | 2 allocs: 576 B | 2 allocs: 576 B |
| downconvert and correlate/CPU/Int16 | 2 allocs: 576 B | 2 allocs: 576 B |
| downconvert and correlate/CPU/Int16 4ant | 2 allocs: 848 B | 2 allocs: 848 B |
| downconvert and correlate/CPU/Int32 | 2 allocs: 576 B | 2 allocs: 576 B |
| fused kernel/1-ant dynamic taps | 0 allocs: 0 B | 0 allocs: 0 B |
| fused kernel/1-ant static taps | 0 allocs: 0 B | 0 allocs: 0 B |
| fused kernel/4-ant dynamic taps | 0 allocs: 0 B | 0 allocs: 0 B |
| fused kernel/4-ant static taps | 0 allocs: 0 B | 0 allocs: 0 B |
| fused tuple kernel/1-ant N=2 | 0 allocs: 0 B | 0 allocs: 0 B |
| fused tuple kernel/1-ant N=3 | 0 allocs: 0 B | 0 allocs: 0 B |
| fused tuple kernel/2-ant N=2 | 0 allocs: 0 B | 0 allocs: 0 B |
| fused tuple kernel/2-ant N=3 | 0 allocs: 0 B | 0 allocs: 0 B |
| fused tuple kernel/4-ant N=2 | 0 allocs: 0 B | 0 allocs: 0 B |
| fused tuple kernel/4-ant N=3 | 0 allocs: 0 B | 0 allocs: 0 B |
| track/1. Float32/2K – track | 9 allocs: 944 B | 9 allocs: 944 B |
| track/1. Float32/2K – track! | 1 allocs: 26 B | 0 allocs: 0 B |
| track/2. L1 8sat/5K – track | 10 allocs: 4656 B | 10 allocs: 4656 B |
| track/2. L1 8sat/5K – track! | 0 allocs: 0 B | 0 allocs: 0 B |
| track/2. L1 8sat/5K – track! Int16 | 13 allocs: 1056 B | 13 allocs: 1056 B |
| track/2. L1 8sat/5K – track! OneBit | 45 allocs: 2736 B | 45 allocs: 2736 B |
| track/2. L1 8sat/5K – track!-threaded | 0 allocs: 0 B | 0 allocs: 0 B |
| track/2. L1 8sat/5K – track-threaded | 10 allocs: 4656 B | 10 allocs: 4656 B |
| track/3. E1B 4sat/25K – track | 10 allocs: 3056 B | 10 allocs: 3056 B |
| track/3. E1B 4sat/25K – track! | 0 allocs: 0 B | 0 allocs: 0 B |
| track/3. E1B 4sat/25K – track! Int16 | 5 allocs: 416 B | 5 allocs: 416 B |
| track/3. E1B 4sat/25K – track!-threaded | 0 allocs: 0 B | 0 allocs: 0 B |
| track/3. E1B 4sat/25K – track-threaded | 10 allocs: 3056 B | 10 allocs: 3056 B |
| track/4. 8L1+8E1B/25K – track | 26 allocs: 10464 B | 26 allocs: 10464 B |
| track/4. 8L1+8E1B/25K – track! | 0 allocs: 0 B | 0 allocs: 0 B |
| track/4. 8L1+8E1B/25K – track!-threaded | 0 allocs: 0 B | 0 allocs: 0 B |
| track/4. 8L1+8E1B/25K – track-threaded | 26 allocs: 10464 B | 26 allocs: 10464 B |
| track/5. multi-signal N=1/5K – track | 12 allocs: 1024 B | 9 allocs: 944 B |
| track/5. multi-signal N=1/5K – track! | 3 allocs: 80 B | 0 allocs: 0 B |
| track/6. multi-signal N=2/5K – track | 15 allocs: 1568 B | 9 allocs: 1408 B |
| track/6. multi-signal N=2/5K – track! | 6 allocs: 160 B | 0 allocs: 0 B |
| track/7. multi-signal N=3/5K – track | 18 allocs: 2000 B | 9 allocs: 1760 B |
| track/7. multi-signal N=3/5K – track! | 9 allocs: 240 B | 0 allocs: 0 B |
| track/8. L1CA presync 2 blk – track! | 13 allocs: 480 B | 7 allocs: 320 B |
| track/8. L1CA presync 20 blk – track! | 67 allocs: 1920 B | 7 allocs: 320 B |
| track/8. L1CA synced 2 blk – track! | 7 allocs: 320 B | 7 allocs: 320 B |
| track/8. L1CA synced 20 blk – track! | 7 allocs: 320 B | 7 allocs: 320 B |
| time_to_load | 196 allocs: 13984 B | 196 allocs: 13984 B |
Benchmark Results (minimum time) — ubuntu-latestReporting the minimum over all samples (robust to shared-runner contention), not the median. Alternative backends vs Float32 (
|
| Scenario | F32 | I16 | 1b | 2b | ×I16 | ×1b | ×2b |
|---|---|---|---|---|---|---|---|
| 4-antenna @ 5 MHz | 12.2 μs | 6.26 μs | 5.21 μs | 9.73 μs | 1.94 ✅ | 2.34 ✅ | 1.25 ✅ |
| GPS L1CA, 8 sats @ 40 MHz | 331.0 μs | 130.0 μs | 71.1 μs | 119.0 μs | 2.54 ✅ | 4.65 ✅ | 2.78 ✅ |
| GPS L1CA, 8 sats @ 5 MHz | 49.3 μs | 28.0 μs | 20.0 μs | 27.7 μs | 1.76 ✅ | 2.47 ✅ | 1.78 ✅ |
| Galileo E1B, 4 sats @ 25 MHz | 122.0 μs | 56.0 μs | 2.18 ✅ | ||||
| dynamic taps @ 5 MHz (kernel) | 5.79 μs | 3.42 μs | 2.02 μs | 2.79 μs | 1.7 ✅ | 2.87 ✅ | 2.08 ✅ |
| multi-signal N=3 @ 5 MHz | 9.52 μs | 5.92 μs | 5.02 μs | 7.06 μs | 1.61 ✅ | 1.9 ✅ | 1.35 ✅ |
Time benchmarks (base vs PR head)
Ratio = 0640d0e… / 8ffedce…: >1 means the PR is faster. ✅ ≥ 5 % faster,
| 0640d0e… | 8ffedce… | 0640d0e… / 8ffedce… | |
|---|---|---|---|
| downconvert and correlate/CPU/Float32 | 2.42 μs | 2.33 μs | 1.04 |
| downconvert and correlate/CPU/Float32 4ant | 4.71 μs | 4.72 μs | 0.998 |
| downconvert and correlate/CPU/Float64 | 2.76 μs | 2.76 μs | 1.0 |
| downconvert and correlate/CPU/Int16 | 2.43 μs | 2.42 μs | 1.0 |
| downconvert and correlate/CPU/Int16 4ant | 5.17 μs | 5.18 μs | 0.998 |
| downconvert and correlate/CPU/Int32 | 2.42 μs | 2.39 μs | 1.01 |
| fused kernel/1-ant dynamic taps | 2.16 μs | 2.16 μs | 1.0 |
| fused kernel/1-ant static taps | 1.88 μs | 1.87 μs | 1.0 |
| fused kernel/4-ant dynamic taps | 5.33 μs | 5.26 μs | 1.01 |
| fused kernel/4-ant static taps | 4.16 μs | 4.15 μs | 1.0 |
| fused tuple kernel/1-ant N=2 | 2.35 μs | 2.35 μs | 1.0 |
| fused tuple kernel/1-ant N=3 | 2.82 μs | 2.85 μs | 0.989 |
| fused tuple kernel/2-ant N=2 | 3.65 μs | 3.59 μs | 1.02 |
| fused tuple kernel/2-ant N=3 | 4.59 μs | 4.54 μs | 1.01 |
| fused tuple kernel/4-ant N=2 | 6.01 μs | 6.0 μs | 1.0 |
| fused tuple kernel/4-ant N=3 | 9.79 μs | 9.79 μs | 1.0 |
| track/1. Float32/2K – track | 2.57 μs | 2.55 μs | 1.01 |
| track/1. Float32/2K – track! | 2.65 μs | 2.62 μs | 1.01 |
| track/2. L1 8sat/5K – track | 47.7 μs | 47.6 μs | 1.0 |
| track/2. L1 8sat/5K – track! | 47.1 μs | 47.3 μs | 0.994 |
| track/2. L1 8sat/5K – track! Int16 | 29.2 μs | 28.1 μs | 1.04 |
| track/2. L1 8sat/5K – track! OneBit | 20.1 μs | 20.0 μs | 1.0 |
| track/2. L1 8sat/5K – track!-threaded | 48.3 μs | 47.3 μs | 1.02 |
| track/2. L1 8sat/5K – track-threaded | 47.8 μs | 47.8 μs | 1.0 |
| track/3. E1B 4sat/25K – track | 119.0 μs | 118.0 μs | 1.01 |
| track/3. E1B 4sat/25K – track! | 118.0 μs | 117.0 μs | 1.01 |
| track/3. E1B 4sat/25K – track! Int16 | 55.0 μs | 55.1 μs | 0.999 |
| track/3. E1B 4sat/25K – track!-threaded | 119.0 μs | 117.0 μs | 1.01 |
| track/3. E1B 4sat/25K – track-threaded | 119.0 μs | 118.0 μs | 1.01 |
| track/4. 8L1+8E1B/25K – track | 437.0 μs | 433.0 μs | 1.01 |
| track/4. 8L1+8E1B/25K – track! | 435.0 μs | 431.0 μs | 1.01 |
| track/4. 8L1+8E1B/25K – track!-threaded | 434.0 μs | 432.0 μs | 1.0 |
| track/4. 8L1+8E1B/25K – track-threaded | 436.0 μs | 434.0 μs | 1.01 |
| track/5. multi-signal N=1/5K – track | 6.23 μs | 6.12 μs | 1.02 |
| track/5. multi-signal N=1/5K – track! | 6.36 μs | 6.13 μs | 1.04 |
| track/6. multi-signal N=2/5K – track | 8.6 μs | 8.43 μs | 1.02 |
| track/6. multi-signal N=2/5K – track! | 8.67 μs | 8.6 μs | 1.01 |
| track/7. multi-signal N=3/5K – track | 10.9 μs | 10.6 μs | 1.03 |
| track/7. multi-signal N=3/5K – track! | 11.1 μs | 10.9 μs | 1.02 |
| track/8. L1CA presync 2 blk – track! | 11.8 μs | 11.6 μs | 1.02 |
| track/8. L1CA presync 20 blk – track! | 114.0 μs | 113.0 μs | 1.02 |
| track/8. L1CA synced 2 blk – track! | 11.5 μs | 11.5 μs | 0.999 |
| track/8. L1CA synced 20 blk – track! | 111.0 μs | 111.0 μs | 0.997 |
| time_to_load | 101.0 μs | 99.8 μs | 1.01 |
Memory benchmarks (base vs PR head)
| 0640d0e… | 8ffedce… | |
|---|---|---|
| downconvert and correlate/CPU/Float32 | 2 allocs: 576 B | 2 allocs: 576 B |
| downconvert and correlate/CPU/Float32 4ant | 2 allocs: 848 B | 2 allocs: 848 B |
| downconvert and correlate/CPU/Float64 | 2 allocs: 576 B | 2 allocs: 576 B |
| downconvert and correlate/CPU/Int16 | 2 allocs: 576 B | 2 allocs: 576 B |
| downconvert and correlate/CPU/Int16 4ant | 2 allocs: 848 B | 2 allocs: 848 B |
| downconvert and correlate/CPU/Int32 | 2 allocs: 576 B | 2 allocs: 576 B |
| fused kernel/1-ant dynamic taps | 0 allocs: 0 B | 0 allocs: 0 B |
| fused kernel/1-ant static taps | 0 allocs: 0 B | 0 allocs: 0 B |
| fused kernel/4-ant dynamic taps | 0 allocs: 0 B | 0 allocs: 0 B |
| fused kernel/4-ant static taps | 0 allocs: 0 B | 0 allocs: 0 B |
| fused tuple kernel/1-ant N=2 | 0 allocs: 0 B | 0 allocs: 0 B |
| fused tuple kernel/1-ant N=3 | 0 allocs: 0 B | 0 allocs: 0 B |
| fused tuple kernel/2-ant N=2 | 0 allocs: 0 B | 0 allocs: 0 B |
| fused tuple kernel/2-ant N=3 | 0 allocs: 0 B | 0 allocs: 0 B |
| fused tuple kernel/4-ant N=2 | 0 allocs: 0 B | 0 allocs: 0 B |
| fused tuple kernel/4-ant N=3 | 0 allocs: 0 B | 0 allocs: 0 B |
| track/1. Float32/2K – track | 9 allocs: 944 B | 9 allocs: 944 B |
| track/1. Float32/2K – track! | 1 allocs: 26 B | 0 allocs: 0 B |
| track/2. L1 8sat/5K – track | 10 allocs: 4536 B | 10 allocs: 4536 B |
| track/2. L1 8sat/5K – track! | 0 allocs: 0 B | 0 allocs: 0 B |
| track/2. L1 8sat/5K – track! Int16 | 13 allocs: 1056 B | 13 allocs: 1056 B |
| track/2. L1 8sat/5K – track! OneBit | 45 allocs: 2736 B | 45 allocs: 2736 B |
| track/2. L1 8sat/5K – track!-threaded | 0 allocs: 0 B | 0 allocs: 0 B |
| track/2. L1 8sat/5K – track-threaded | 10 allocs: 4536 B | 10 allocs: 4536 B |
| track/3. E1B 4sat/25K – track | 10 allocs: 2808 B | 10 allocs: 2808 B |
| track/3. E1B 4sat/25K – track! | 0 allocs: 0 B | 0 allocs: 0 B |
| track/3. E1B 4sat/25K – track! Int16 | 5 allocs: 416 B | 5 allocs: 416 B |
| track/3. E1B 4sat/25K – track!-threaded | 0 allocs: 0 B | 0 allocs: 0 B |
| track/3. E1B 4sat/25K – track-threaded | 10 allocs: 2808 B | 10 allocs: 2808 B |
| track/4. 8L1+8E1B/25K – track | 26 allocs: 10352 B | 26 allocs: 10352 B |
| track/4. 8L1+8E1B/25K – track! | 0 allocs: 0 B | 0 allocs: 0 B |
| track/4. 8L1+8E1B/25K – track!-threaded | 0 allocs: 0 B | 0 allocs: 0 B |
| track/4. 8L1+8E1B/25K – track-threaded | 26 allocs: 10352 B | 26 allocs: 10352 B |
| track/5. multi-signal N=1/5K – track | 12 allocs: 1024 B | 9 allocs: 944 B |
| track/5. multi-signal N=1/5K – track! | 3 allocs: 80 B | 0 allocs: 0 B |
| track/6. multi-signal N=2/5K – track | 15 allocs: 1568 B | 9 allocs: 1408 B |
| track/6. multi-signal N=2/5K – track! | 6 allocs: 160 B | 0 allocs: 0 B |
| track/7. multi-signal N=3/5K – track | 18 allocs: 2000 B | 9 allocs: 1760 B |
| track/7. multi-signal N=3/5K – track! | 9 allocs: 240 B | 0 allocs: 0 B |
| track/8. L1CA presync 2 blk – track! | 13 allocs: 480 B | 7 allocs: 320 B |
| track/8. L1CA presync 20 blk – track! | 67 allocs: 1920 B | 7 allocs: 320 B |
| track/8. L1CA synced 2 blk – track! | 7 allocs: 320 B | 7 allocs: 320 B |
| track/8. L1CA synced 20 blk – track! | 7 allocs: 320 B | 7 allocs: 320 B |
| time_to_load | 145 allocs: 11216 B | 145 allocs: 11216 B |
…ts root cause (#198) Investigating #198 (track! "allocates per completed integration") found the reported ~40 KB/completion to be a measurement artifact — `@allocated track!(rand(ComplexF32, n), …)` counts the `rand` input buffer (n×8 B = 40 KB per 5000-sample code period), which grows with signal length. With the buffer hoisted out and measured from a typed call site, track! is flat: the single-threaded backend is 0 B at any completion count (pinned by the existing pre-sync allocation guard at 2 and 20 blocks, on top of #197 which removes the _buffer_find_bit Core.Box that otherwise leaks ~80 B/block under a multi-threaded runtime). The one genuine residual is on the threaded backend: ~64 B per completed code block per group, but only with >1 thread AND >1 satellite, since track! launches Polyester's @Batch once per code block. Correct the docstrings that mislabeled this as an "irreducible ~160 B per system per call" cost, and document the actual root cause: Polyester's @Batch is allocation-free only when the parallel closure touches nothing but Arrays and isbits values; it pays a small per-launch allocation to root any other non-isbits object. The culprit is the GNSS signal (e.g. GPSL1CA is not isbits — it wraps a Matrix code table and a non-isbits SignalLUT), which each satellite's code-replica generation must touch. The only way to remove it is to move code generation to a serial pre-pass (allocation-free but serializes ~30% of the work and slows the threaded pipeline), which is not worth ~64 B; the thread-over-PRNs design is kept. Also cross-reference #198 in the pre-sync allocation guard's rationale (it already pins allocation flat across completion counts). The track! benchmark size is intentionally left at 2000 samples. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ts root cause (#198) Investigating #198 (track! "allocates per completed integration") found the reported ~40 KB/completion to be a measurement artifact — `@allocated track!(rand(ComplexF32, n), …)` counts the `rand` input buffer (n×8 B = 40 KB per 5000-sample code period), which grows with signal length. With the buffer hoisted out and measured from a typed call site, track! is flat: the single-threaded backend is 0 B at any completion count (pinned by the existing pre-sync allocation guard at 2 and 20 blocks, on top of #197 which removes the _buffer_find_bit Core.Box that otherwise leaks ~80 B/block under a multi-threaded runtime). The one genuine residual is on the threaded backend: ~64 B per completed code block per group, but only with >1 thread AND >1 satellite, since track! launches Polyester's @Batch once per code block. Correct the docstrings that mislabeled this as an "irreducible ~160 B per system per call" cost, and document the actual root cause: Polyester's @Batch is allocation-free only when the parallel closure touches nothing but Arrays and isbits values; it pays a small per-launch allocation to root any other non-isbits object. The culprit is the GNSS signal (e.g. GPSL1CA is not isbits — it wraps a Matrix code table and a non-isbits SignalLUT), which each satellite's code-replica generation must touch. The only way to remove it is to move code generation to a serial pre-pass (allocation-free but serializes ~30% of the work and slows the threaded pipeline), which is not worth ~64 B; the thread-over-PRNs design is kept. Also cross-reference #198 in the pre-sync allocation guard's rationale (it already pins allocation flat across completion counts). The track! benchmark size is intentionally left at 2000 samples. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ts root cause (#198) Investigating #198 (track! "allocates per completed integration") found the reported ~40 KB/completion to be a measurement artifact — `@allocated track!(rand(ComplexF32, n), …)` counts the `rand` input buffer (n×8 B = 40 KB per 5000-sample code period), which grows with signal length. With the buffer hoisted out and measured from a typed call site, track! is flat: the single-threaded backend is 0 B at any completion count (pinned by the existing pre-sync allocation guard at 2 and 20 blocks, on top of #197 which removes the _buffer_find_bit Core.Box that otherwise leaks ~80 B/block under a multi-threaded runtime). The one genuine residual is on the threaded backend: ~64 B per completed code block per group, but only with >1 thread AND >1 satellite, since track! launches Polyester's @Batch once per code block. Correct the docstrings that mislabeled this as an "irreducible ~160 B per system per call" cost, and document the actual root cause: Polyester's @Batch is allocation-free only when the parallel closure touches nothing but Arrays and isbits values; it pays a small per-launch allocation to root any other non-isbits object. The culprit is the GNSS signal (e.g. GPSL1CA is not isbits — it wraps a Matrix code table and a non-isbits SignalLUT), which each satellite's code-replica generation must touch. The only way to remove it is to move code generation to a serial pre-pass (allocation-free but serializes ~30% of the work and slows the threaded pipeline), which is not worth ~64 B; the thread-over-PRNs design is kept. Also cross-reference #198 in the pre-sync allocation guard's rationale (it already pins allocation flat across completion counts). The track! benchmark size is intentionally left at 2000 samples. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ts root cause (#198) Investigating #198 (track! "allocates per completed integration") found the reported ~40 KB/completion to be a measurement artifact — `@allocated track!(rand(ComplexF32, n), …)` counts the `rand` input buffer (n×8 B = 40 KB per 5000-sample code period), which grows with signal length. With the buffer hoisted out and measured from a typed call site, track! is flat: the single-threaded backend is 0 B at any completion count (pinned by the existing pre-sync allocation guard at 2 and 20 blocks, on top of #197 which removes the _buffer_find_bit Core.Box that otherwise leaks ~80 B/block under a multi-threaded runtime). The one genuine residual is on the threaded backend: ~64 B per completed code block per group, but only with >1 thread AND >1 satellite, since track! launches Polyester's @Batch once per code block. Correct the docstrings that mislabeled this as an "irreducible ~160 B per system per call" cost, and document the actual root cause: Polyester's @Batch is allocation-free only when the parallel closure touches nothing but Arrays and isbits values; it pays a small per-launch allocation to root any other non-isbits object. The culprit is the GNSS signal (e.g. GPSL1CA is not isbits — it wraps a Matrix code table and a non-isbits SignalLUT), which each satellite's code-replica generation must touch. The only way to remove it is to move code generation to a serial pre-pass (allocation-free but serializes ~30% of the work and slows the threaded pipeline), which is not worth ~64 B; the thread-over-PRNs design is kept. Also cross-reference #198 in the pre-sync allocation guard's rationale (it already pins allocation flat across completion counts). The track! benchmark size is intentionally left at 2000 samples. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Why
PR #194 fixed a pre-sync per-code-block allocation in
track!, but it was accidentally merged into the feature branchss/two-bit-downconvert-and-correlateinstead ofmaster. As a resultmasternever received the fix.This PR re-targets that work at
masterby cherry-picking #194's two commits (95c61b7,c2c9b4e) onto master. They apply cleanly with no conflicts, and the resulting diff is identical to the original #194 diff.What
src/bit_buffer.jl: hoistsync.polarityinto a plain local so the reduce-closure in_buffer_find_bitno longer capturessync, avoiding the per-code-blockCore.Box/SyncResultboxing on the pre-sync hot path.test/track_in_place.jl: guard against pre-sync per-code-block allocation regressions.benchmark/benchmarks.jl: add coverage for the pre-sync path.The duplicate commits on
ss/two-bit-downconvert-and-correlateare left in place; they'll fold in harmlessly when that branch eventually merges.🤖 Generated with Claude Code