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]

Reply via email to