dav1d ARM Assembly Optimizations for AV1 Decoding
This article explores how the open-source dav1d decoder delivers industry-leading AV1 video playback speeds on ARM-powered devices, from smartphones to servers. By bypassing compiler auto-vectorization in favor of hand-tuned assembly, dav1d maximizes the architectural features of ARMv7 and AArch64 architectures. The following breakdown examines the key assembly-level techniques employed—including custom ARM NEON SIMD implementations, aggressive register allocation, pipeline-aware instruction scheduling, and specialized algorithms for AV1 in-loop filters.
Hand-Crafted ARM NEON SIMD Vectorization
Compilers often struggle to auto-vectorize complex video decoding routines due to pointer aliasing, non-trivial branching, and boundary conditions. dav1d addresses this by implementing all compute-intensive inner loops directly in hand-written assembly using ARM NEON SIMD (Single Instruction, Multiple Data).
NEON provides 128-bit vector registers capable of processing 16 8-bit integers or 8 16-bit integers per instruction. dav1d leverages this across core decoding tasks:
- Intra and Inter Prediction: Pixel block
predictions, fractional pel interpolation, and weighted averaging are
calculated simultaneously across entire rows or blocks using vector
instructions like
uaddl,umlsl, anduxtl. - Inverse Transforms (IDCT/ADST): dav1d converts 2D spatial transforms into vectorized 1D row and column operations. By processing 8 or 16 pixels per vector lane, it completes multi-stage butterfly operations in fewer clock cycles.
Register Allocation and Spill Reduction on AArch64
AArch64 provides 31 general-purpose 64-bit registers
(x0–x30) and 32 128-bit SIMD/floating-point
registers (v0–v31). dav1d's assembly routines
are designed to maximize the occupancy of these registers.
By keeping coefficients, intermediate transform values, filter
coefficients, and prediction states entirely within the
v0–v31 vector registers, dav1d eliminates
stack spilling (saving and restoring variables to memory). This
drastically cuts down on L1 cache interactions and avoids load-to-use
pipeline stalls, which is critical for low-power mobile cores with
limited cache bandwidth.
Loop Filter Optimization: CDEF and Loop Restoration
AV1 features three distinct in-loop filtering stages that make decoding computationally heavy: the Deblocking Filter, the Constrained Directional Enhancement Filter (CDEF), and Loop Restoration (Wiener and Self-Guided filters). dav1d implements bespoke assembly strategies for each:
- CDEF Direction Search and Filtering: CDEF requires
determining the primary direction of edges within an 8x8 block and then
applying a directional low-pass filter. dav1d calculates the directional
variance and cross-directional differences using vectorized horizontal
additions (
saddlv,uaddlv) and vector clamping (smin,smax,sqrshrn). - Wiener and Self-Guided Restoration: These filters
employ 2D separable symmetric convolutions. In assembly, dav1d splits
the operations into horizontal passes that remain in registers, followed
by vertical passes using transposed values. Intermediate rounding and
shifts are mapped directly to ARM instructions like
sqrdmulh(Signed Saturating Rounding Doubling Multiply High) andrshrn(Rounding Shift Right Narrow), which perform rounding and clamping in a single instruction.
In-Register Matrix Transposition
Inverse transform stages and directional filtering require frequent transpositions of 4x4, 8x8, 16x16, and 32x32 blocks of pixel data. Writing data back to cache to read it column-wise is slow.
dav1d relies on ARM NEON permute and interleave instructions, specifically:
zip1/zip2for interleaving high and low halves of vectors.trn1/trn2for transposing paired 8-bit or 16-bit lanes.ext(Extract) for rotating elements across vector boundaries without touching memory.
These operations allow entire 8x8 blocks of transform coefficients to be transposed in-place within the vector register file in a handful of cycles.
Instruction Scheduling and Latency Hiding
Modern ARM cores, such as ARM Cortex-A and Apple Silicon, rely on wide out-of-order execution pipelines. dav1d’s assembly is tuned to keep execution units fully saturated:
- Independent Instruction Interleaving: Rather than
executing back-to-back operations on the same vector register, dav1d
unrolls loops and interleaves operations across multiple independent
registers. For example, while one instruction waits for the latency of a
vector multiply-accumulate (
mla), the execution engine processes loads or shifts for an adjacent pixel block. - Minimizing Conditional Branches: Branch
mispredictions flush the CPU pipeline. dav1d replaces conditional code
paths within assembly with branchless vector masks, such as bitwise
selections (
bif,bit,bsl) and vector comparisons (cmeq,cmgt), allowing uniform execution flow regardless of video content.
Efficient Film Grain Synthesis
AV1 uses grain synthesis to simulate film grain at the playback stage, avoiding the bit-cost of encoding noise. dav1d implements the PRNG (Pseudo-Random Number Generator) and Gaussian noise application directly in NEON assembly, vectorizing the calculation of random grain patterns across entire macroblocks to make this post-processing step virtually free on ARM processors.