Repository navigation
perf(mlow): precompute FFT twiddles and reuse the recursion buffer - #38
Closed
luanluciano93 wants to merge 1 commit into
Closed
luanluciano93 wants to merge 1 commit into
luanluciano93 wants to merge 1 commit into
Conversation
fftRec called math.Cos/math.Sin once per (k,j) pair in the naive DFT leg and
once per (k,q) pair in the mixed-radix leg, even though a transform of length n
only has n distinct roots of unity. In a CPU profile of a 60 ms frame through
MlowEncoder, math.cos and math.sin together accounted for ~39% of the whole
media path, with fftRec at ~47% cumulative.
It also allocated a fresh make([]cpx, n) at every node of the recursion tree:
~3,800 allocations per 60 ms frame, or roughly 28 MB/s of garbage per concurrent
call.
This caches the roots per (size, sign) in a sync.Map and indexes them by
(k*j) mod n, and carves the recursion scratch out of a single pooled buffer.
Indexing by the remainder is also more accurate than the previous float32 angle
product, whose error grew with k*j.
Measured on an AMD EPYC 9354P, go1.26, -benchtime 200x -count=5, medians:
BenchmarkMlowEncodeFrame 7.18ms -> 2.76ms (2.6x)
BenchmarkPercFFT 267us -> 53us (5.0x)
allocs/frame (encoder) 3855 -> 1966
allocs/op (rfft) 129 -> 3
The bitstream changes very slightly, as the more accurate twiddles flip a few
quantiser decisions: 200 encoded speech frames came to 34,161 bytes instead of
34,181. The output stays valid — the existing mlow tests pass, MlowDecoder reads
all 200 frames back with no empty frames and no NaNs, and three real WhatsApp
calls placed with this patch carried audio in both directions, with the far end
hearing the transmitted tones cleanly.
Benchmarks are added for both levels so the change can be re-measured. The
encoder benchmark feeds voice-like audio on purpose: on silence the VAD takes a
much shorter path and the numbers flatter the encoder.
Author
|
Closing as a duplicate of #30, which predates this by a month and is the better change — it shares one twiddle table across all recursion levels, also removes the I have moved my reproduction and the real-call validation of the bitstream change into a comment on #30. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fftReccallsmath.Cos/math.Sinonce per(k,j)pair in the naive DFT leg and once per(k,q)pair in the mixed-radix leg, even though a transform of lengthnonly hasndistinct roots of unity. It also allocates a freshmake([]cpx, n)at every node of the recursion tree.I found this while profiling a WhatsApp call bridge built on meowcaller. In a CPU profile of one 60 ms frame through
MlowEncoder:math.cosmath.sinmlow.fftRec(cumulative)So roughly 39% of the entire media path was spent recomputing values that never change.
The change
(size, sign)in async.Map, indexed by(k*j) mod n;Indexing by the remainder is also slightly more accurate than the previous
sign*2*pi*k*j/nfloat32 product, whose error grew withk*j.Numbers
AMD EPYC 9354P, go1.26,
-benchtime 200x -count=5, medians:After the change
math.cosdrops to 2.4% andfftRecto 21.9% cumulative; what is left at the top is real codec work (addPulse,celpDotProd,SmplPitch).Correctness
The more accurate twiddles flip a few quantiser decisions, so the bitstream changes very slightly — 200 encoded speech frames came to 34,161 bytes instead of 34,181. Everything I could check says the output is still valid:
mlowtests pass unmodified;MlowDecoderreads back all 200 frames produced by the patched encoder: no empty frames, no NaNs, every frame carrying energy;If you would rather keep the bitstream bit-identical, this cannot be done with a table at all, since the old code's angle is a float32 product rather than a value with period
n. I would be happy to put the table behind a build tag or an option instead, if you prefer.Benchmarks
I added
mlow/fft_bench_test.goat both levels so the change can be re-measured. The encoder benchmark synthesises voice-like audio on purpose: on silence the VAD takes a much shorter path and the numbers flatter the encoder.