bits: replace per-probe multiplies with one division in lc3_get_symbol - #89
bits: replace per-probe multiplies with one division in lc3_get_symbol#89parthvelobyte wants to merge 1 commit into
Conversation
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
28b572b to
37c97c2
Compare
|
@googlebot I signed it! |
7bd946f to
303c2f6
Compare
The binary search over the ac model compares ac->low against
range * symbols[s].low at every probe, which puts a data-dependent
multiply on the decoder's critical path for every symbol read.
For unsigned integers with range >= 1,
low < range * L <=> low / range < L
(range*q <= low < range*(q+1), so low < range*L iff q < L), so the
quotient computed once turns every probe into a plain table compare.
The final low/range update is unchanged.
Encoded and decoded outputs are byte-identical before and after the
change across a 180 s corpus at 32/96 kbps, 7.5 and 10 ms frames.
Decoder wall time measures 3-5% faster on Apple M2 Pro (clang 15,
-O3 -ffast-math, interleaved A/B, median of 8 rounds).
303c2f6 to
c979716
Compare
|
Thanks for the analysis and PR! While replacing the 4 dependent multiplications with 1 runtime integer division (
Could we guard this optimization behind an architecture/compiler macro check (e.g., only enabling it on |
While profiling the float decoder on Apple M2 Pro (release flags,
-O3 -ffast-math -flto), nearly all decode time attributes tolc3_spec_decode, and within it the arithmetic-decoder symbol reads. Looking atlc3_get_symbol(): the binary search comparesac->lowagainstrange * symbols[s].lowat every probe, and since each probe'ssdepends on the previous compare, that is up to five dependent multiplies on the critical path of every symbol.The multiplies can be replaced by one division computed before the search:
Proof: let
q = low / range, sorange*q <= low < range*(q+1). Ifq < Lthenlow < range*(q+1) <= range*L. Conversely iflow < range*Lthenrange*q <= low < range*L, soq < L. ∎After the udiv, every probe is a plain table compare with no chained arithmetic, and the four compares are independent of each other except through
s. The finalac->low -= range * symbols[s].low/ac->rangeupdate is unchanged.Why the division is always defined and nothing wraps:
ac->rangeis initialized to0xffffffand every renormalization restoresac->range >= 0x10000, sorange = (ac->range >> 10) & 0xffffis in[0x40, 0x3fff];ac->lowis masked to 24 bits everywhere;symbols[s].low < 2^16. All products stay below2^30. The identity is also machine-checked with z3 over exactly this domain — script below, proves in 0.1 s.Correctness: encoded and decoded outputs are byte-identical to the unpatched build (SHA-256 on every artifact) across a deterministic 180 s 48 kHz corpus at 32 and 96 kbps, 7.5 ms and 10 ms frames.
Performance: decoder wall time 3–5% faster on Apple M2 Pro (Apple clang 15, release flags), measured with the two libraries strictly alternating in the same process environment, median of 6–10 rounds.
lc3_put_symboland the encoder are untouched.z3 proof script
reproduce the benchmark
Any fixed 48 kHz mono WAV works as a corpus (I used 180 s of deterministic
seeded noise + tones). With
baseandpatchbuilds ofbin/dlc3and anencoded
c.lc3:🤖 Generated with Claude Code