Skip to content

perf(bit-buffer): fix pre-sync per-code-block allocation in track! (re-target of #194 to master)#197

Merged
zsoerenm merged 2 commits into
masterfrom
merge-194-into-master
Jul 8, 2026
Merged

perf(bit-buffer): fix pre-sync per-code-block allocation in track! (re-target of #194 to master)#197
zsoerenm merged 2 commits into
masterfrom
merge-194-into-master

Conversation

@zsoerenm

@zsoerenm zsoerenm commented Jul 8, 2026

Copy link
Copy Markdown
Member

Why

PR #194 fixed a pre-sync per-code-block allocation in track!, but it was accidentally merged into the feature branch ss/two-bit-downconvert-and-correlate instead of master. As a result master never received the fix.

This PR re-targets that work at master by 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: hoist sync.polarity into a plain local so the reduce-closure in _buffer_find_bit no longer captures sync, avoiding the per-code-block Core.Box/SyncResult boxing 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-correlate are left in place; they'll fold in harmlessly when that branch eventually merges.

🤖 Generated with Claude Code

zsoerenm and others added 2 commits July 8, 2026 21:57
…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>
@zsoerenm
zsoerenm force-pushed the merge-194-into-master branch from cf1fbc8 to 8ffedce Compare July 8, 2026 19:57
@codecov

codecov Bot commented Jul 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.63%. Comparing base (090d6a5) to head (8ffedce).
⚠️ Report is 6 commits behind head on master.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results (minimum time) — macos-14

Reporting the minimum over all samples (robust to shared-runner contention), not the median.

Alternative backends vs Float32 (track!, PR head)

Legend — backends: F32 Float32 (default) · I16 Int16 · 1b OneBit · 2b TwoBit (2-bit measurement + 2-bit carrier). Time columns are the minimum track! time; ×B = F32 / B (so >1 ⇒ backend B is faster than Float32), ✅ ≥ 5 % faster, ⚠️ ≥ 5 % slower. 1b/2b are BPSK-only, so their cells are blank for CBOC (Galileo E1B) scenarios.

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, ⚠️ ≥ 5 % slower. A blank cell means the benchmark exists on only one revision (🆕 = new on the PR, 🗑 = removed).

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

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results (minimum time) — ubuntu-latest

Reporting the minimum over all samples (robust to shared-runner contention), not the median.

Alternative backends vs Float32 (track!, PR head)

Legend — backends: F32 Float32 (default) · I16 Int16 · 1b OneBit · 2b TwoBit (2-bit measurement + 2-bit carrier). Time columns are the minimum track! time; ×B = F32 / B (so >1 ⇒ backend B is faster than Float32), ✅ ≥ 5 % faster, ⚠️ ≥ 5 % slower. 1b/2b are BPSK-only, so their cells are blank for CBOC (Galileo E1B) scenarios.

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, ⚠️ ≥ 5 % slower. A blank cell means the benchmark exists on only one revision (🆕 = new on the PR, 🗑 = removed).

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

@zsoerenm
zsoerenm merged commit 3dbf778 into master Jul 8, 2026
12 checks passed
@zsoerenm
zsoerenm deleted the merge-194-into-master branch July 8, 2026 20:16
zsoerenm added a commit that referenced this pull request Jul 9, 2026
…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>
zsoerenm added a commit that referenced this pull request Jul 9, 2026
…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>
zsoerenm added a commit that referenced this pull request Jul 9, 2026
…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>
zsoerenm added a commit that referenced this pull request Jul 9, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant