Hi all,
I work at UVI, where we make virtual instruments and sample libraries.
Our libraries ship their samples as FLAC, and the sampler streams them
from disk while you play.
Every note that is not already in the cache starts a stream that decodes
FLAC frames on a worker thread. With a lot of voices, decoding is a
noticeable part of the CPU load, so I spent some time profiling libFLAC
on our material.
The result is a series of 7 patches against current master:
https://github.com/UVI-Dev/flac-fork/commits/decode-speedups
Decoding is 36 to 44% faster on Apple M1 and 21 to 43% faster on x86-64,
depending on compiler and material, with bit-identical output. I
measured every commit on its own on both platforms and only kept changes
that are not slower anywhere. Each commit message has its numbers.
In short:
1. Rice parameter 0 partitions go through the fast block reader instead
of one read_unary call per residual. Helps quiet passages (decay tails,
which our samples have plenty of), no change on dense music.
2. The stop bit and the LSBs are dropped from the bit buffer with one
shift instead of two. The buffer goes from one residual to the next
through that chain, so one operation less there shows.
3. undo_channel_coding() tests side_subframe_in_use once per frame
instead of once per sample, so the side-coded loops vectorize with MSVC
and clang-cl (clang on macOS already managed).
4. The "decoded sample fits bps" check no longer exits early, so it
vectorizes too.
5. LPC and fixed subframes are restored while the residual is read,
instead of decoding the residual first and running lpc_restore_signal()
in a second pass.
6. Frame CRC-16 with PMULL folding on arm64.
7. Frame CRC-16 with PCLMULQDQ on x86-64, chosen at run time.
Patch 5 is where most of the gain is. The rice decoding is bound by the
bit position, the restore by the previous samples, and in one loop the
two chains overlap. We got the idea from dr_flac, which does this.
Residuals are still stored for the client.
For orders up to 12 the loop is instantiated per order, and the
prediction of the next sample but its newest tap is computed while the
current sample is restored. That way the sample to sample dependency is
one multiply-add, the shift and the add, whatever the compiler does with
the additions.
The newest sample is kept in a variable, but the 11 older ones only
where there are enough registers. On x86 a window of 12 does not fit,
both MSVC and clang spill it and shift it in memory for every sample, so
there the older samples are read back from the output buffer. On arm64
reading them back costs up to 5%.
So bitreader.c picks the window with FLAC__CPU_X86_64 / FLAC__CPU_IA32:
x86 reads the older samples back from the buffer, other architectures
keep all 12 in variables. It comes down to the number of registers, and
each choice measured best on its own side.
About 6 and 7: the table CRC-16 was 10 to 20% of the decoding time. We
first considered an option to skip it for frames that follow a valid
one, but computing it faster keeps every frame checked and now costs
almost nothing, so there is no such option in this series.
Numbers (ns per sample, all channels, full decode from memory, best of 8
runs, CRC checked):
master series change
Apple M1, clang 15
16-bit samples 5.65 3.18 -43.7%
24-bit samples 6.86 3.91 -43.1%
16-bit music 7.01 4.47 -36.2%
Core Ultra 9 185H, MSVC 19.29
16-bit samples 6.69 4.58 -31.6%
24-bit samples 9.39 5.49 -41.6%
16-bit music 7.83 5.75 -26.5%
Core Ultra 9 185H, clang-cl 23
16-bit samples 5.76 4.02 -30.2%
24-bit samples 7.08 5.12 -27.7%
16-bit music 6.52 5.18 -20.6%
Core Ultra 9 185H, MSVC 19.51 (VS 2026)
16-bit samples 7.66 4.45 -41.8%
24-bit samples 7.91 5.58 -29.4%
16-bit music 9.57 5.42 -43.4%
With MSVC 19.51, master itself is slower than with 19.29 on 16-bit
samples and music (and faster on 24-bit), while the series runs at the
same speed with both. Better compare the absolute numbers there.
Testing: the whole test suite passes (ctest, including the streams test)
on macOS arm64, and the decoded output is identical to master for every
commit on all the sets above. flac -t passes on all the test files on
Windows. The PCLMULQDQ CRC was also checked against a bitwise CRC-16 on
random data.
Every commit builds on its own without new warnings with clang -Wall
-Wextra, and the final state has no new warning with MSVC 19.29 (default
and /W4) and 19.51.
Things I would like your opinion on:
- bitreader.c now knows about LPC prediction (it includes lpc.h), and
the per-order instantiation leans on macros around the deduplicated rice
reader. I tried to keep it close to how the BMI2 variant is done, but
there may be a layout you prefer.
- I have only tested arm64 macOS and x86-64 Windows. Linux/GCC results
would be welcome.
I can split this into separate pull requests if that is easier to
review, starting with 1 to 4 which are small.
Thanks,
--
Olivier Tristan
Research & Development
www.uvi.net
_______________________________________________
flac-dev mailing list -- [email protected]
To unsubscribe send an email to [email protected]