Benchmarks

All benchmarks were run on an Apple M2 Max, macOS 26, single-threaded, with Julia 1.12.6 and AppleAccelerate.jl v0.8.0 master (all tables measured July 2026, after the 2D real FFT / batched FFT / FFT setup-caching work). Times are the minimum of 5 trials.

Single-threaded execution is used to compare kernel quality on a level footing. This is not representative of how OpenBLAS is normally deployed — OpenBLAS scales GEMM across CPU cores, whereas Accelerate offloads large GEMM to a shared SME co-processor that a single thread already saturates. See the GEMM note below for a multi-threaded comparison.

How these benchmarks are generated

Every table on this page is produced by the scripts in test/bench/, driven by run_benchmarks.jl:

julia --project=test/bench test/bench/run_benchmarks.jl          # all suites
julia --project=test/bench test/bench/run_benchmarks.jl dense    # one suite

Each suite runs in its own fresh Julia process so that the OpenBLAS/SuiteSparse baselines are measured before AppleAccelerate is loaded (loading it forwards BLAS/LAPACK through libblastrampoline for the rest of the process). The tables on this page were produced by running the suites one at a time so no two benchmarks compete for the machine. All suites default to a single thread; the dense suite additionally honors a BENCH_THREADS environment variable for thread-scaling sweeps like the GEMM table below:

BENCH_THREADS=4 julia --project=test/bench test/bench/run_benchmarks.jl dense

Array Operations

Performance comparison of vDSP array operations vs Julia Base equivalents (map(Base.f, X) for unary, @simd loops for binary/compound). Source: bench_array.jl.

Unary Math Functions

Transcendental functions show the biggest gains — vDSP is 7–19× faster for sin/cos on Float32:

OpTypeNvDSP (μs)Julia (μs)Speedup
expFloat64100,0001353012.2×
logFloat64100,0001745002.9×
sinFloat64100,0001477315.0×
cosFloat64100,0001137596.7×
sqrtFloat64100,00030592.0×
expFloat32100,000423197.7×
logFloat32100,000563816.8×
sinFloat32100,0003870418.8×
cosFloat32100,0003871518.7×
sqrtFloat32100,00028692.5×

Reductions

sum/maximum/minimum are 1.0–1.2× faster. Note: dot is slower via vDSP because Julia's LinearAlgebra.dot already uses the Accelerate-forwarded BLAS:

OpTypeNvDSP (μs)Julia (μs)Speedup
sumFloat641,000,000901011.1×
maximumFloat641,000,00090971.1×
minimumFloat641,000,00093941.0×
sumFloat321,000,00044541.2×
maximumFloat321,000,00045491.1×
minimumFloat321,000,00044491.1×

Binary Element-wise Ops

Addition and multiplication are memory-bandwidth-bound, so Float64 results hover at parity, while vDSP shows a modest edge for Float32:

OpTypeNvDSP (μs)Julia (μs)Speedup
vaddFloat641,000,0002542521.0×
vmulFloat641,000,0002552401.1× slower
vaddFloat321,000,000891411.6×
vmulFloat321,000,0001351401.0×
Benchmark environment

Julia reference uses map(Base.f, X) for unary ops and @inbounds @simd loops for binary/compound ops. Source: bench_array.jl.

Dense Linear Algebra

Performance comparison of Apple Accelerate vs OpenBLAS. The dense benchmark script loads OpenBLAS first, then switches to Accelerate via LBT, so both are measured in the same process. Source: bench_dense.jl.

GEMM (mul!) — GFLOPS (higher is better)

Single-threaded, Accelerate is 6–13× faster for matrix multiply, with the largest gains for Float32:

TypeNOpenBLASAccelerateSpeedup
Float6464242379.8×
Float64256453497.7×
Float641,024513607.1×
Float644,096522915.6×
Float3264885996.8×
Float322561001,21712.2×
Float321,0241041,39613.5×
Float324,0961061,13610.8×
These are single-threaded numbers — read this before comparing

The table above pins both libraries to a single thread (BLAS.set_num_threads(1)), which is not how OpenBLAS is normally used and understates it substantially.

The two libraries use different execution units on Apple Silicon:

  • OpenBLAS runs GEMM on the CPU's P-cores (NEON), and scales with thread count.
  • Accelerate dispatches large GEMM to the SME/AMX matrix co-processor — a dedicated unit shared per core cluster. This is a CPU-side co-processor, not the GPU (Apple GPUs do not natively support Float64, and Apple documents Accelerate as CPU-based). Because each unit is shared and a single thread on that cluster already saturates it, Accelerate GEMM throughput scales only with the number of co-processors (i.e. the number of core clusters), not with BLAS.set_num_threads beyond that.

Measured on this page's reference machine, an Apple M2 Max (8 P-cores in two clusters + 4 E-cores), Float64/Float32 GEMM at N=4096:

ThreadsOpenBLAS F64OB speedupAccelerate F64OpenBLAS F32Accelerate F32
152.51.0×314.8105.11158.3
2104.42.0×616.2206.72251.8
3152.12.9×615.8299.42236.7
4205.83.9×617.4398.82254.9
6276.25.3×611.7561.12248.7
8354.16.7×618.1715.62240.3

(GFLOPS; higher is better. This is a separate thread-scaling sweep, so its 1-thread column differs slightly from the single-threaded table above due to run-to-run variance.) Two effects are visible. Accelerate doubles from 1→2 threads — the M2 Max has two P-core clusters, so a second thread engages the second SME unit — then stays flat, since there are no more co-processors to recruit. OpenBLAS keeps scaling across all 8 P-cores (~6.7×). On a single-P-cluster chip such as the M4, Accelerate is flat from thread 1 (see the M4 data in issue #132).

So the multiple by which Accelerate leads shrinks as OpenBLAS is given more cores. Accelerate still wins here, but the single-threaded speedups above should be read as a kernel-quality comparison, not as the gap you will see against a fully-threaded OpenBLAS. See Apple's CPU optimization guide for details on the SME engine, and arXiv:2409.18779 for independent benchmarks of matrix-multiply throughput on Apple Silicon's matrix co-processor.

Factorizations (Float64) — time in μs (lower is better)

OperationNOpenBLAS (μs)Accelerate (μs)Speedup
LU2564292202.0×
LU1,02416,95611,1901.5×
LU2,048123,07168,3071.8×
QR5125,5713,4221.6×
QR2,048280,167145,1901.9×
Cholesky256252882.9×
Cholesky1,0249,3132,6883.5×
SVD25611,5336,6091.7×
SVD51268,99030,9732.2×
SVD1,024435,259157,8942.8×

Linear Solve (A\b) — time in μs (lower is better)

TypeNOpenBLAS (μs)Accelerate (μs)Speedup
Float642564581932.4×
Float641,02417,1565,3413.2×
Float642,048123,08137,5013.3×
Float322563121242.5×
Float321,0249,7992,5763.8×
Float322,04865,85514,7734.5×
Benchmark environment

LinearAlgebra.jl v1.12.0 (OpenBLAS 0.3.29). OpenBLAS benchmarked before loading AppleAccelerate; Accelerate benchmarked after using AppleAccelerate forwards BLAS via LBT. Source: bench_dense.jl.

Sparse Linear Algebra

Performance comparison of Apple Sparse Solvers vs SuiteSparse (CHOLMOD/UMFPACK). The sparse benchmark script runs SuiteSparse first (before loading AppleAccelerate), so SuiteSparse uses OpenBLAS internally, then loads AppleAccelerate and re-runs with Apple's sparse solvers. Source: bench_sparse.jl.

Sparse Matrix-Vector Multiply (density=0.01)

SuiteSparse CSC SpMV is 2.4–4.5× faster due to its simpler data layout:

TypeNApple (μs)SuiteSparse (μs)Ratio
Float641,0002370.32×
Float6410,0002,1395120.24×
Float6450,00052,12122,0380.42×
Float321,0002370.31×
Float3210,0002,1064720.22×

QR Factorize + Solve

SuiteSparse LU (\) is modestly faster for Float64 (near parity by N=2000). Apple QR wins for Float32 at N≥1000 (up to 2×):

TypeNApple (μs)SuiteSparse (μs)Speedup
Float645002,4121,9900.83×
Float642,000117,360119,0561.01×
Float645,0002,038,8891,845,1750.91×
Float321,0009,52113,2861.40×
Float322,00057,290116,9952.04×
Float325,000966,0201,820,9231.88×

Cholesky Factorize + Solve

Apple Cholesky is around parity at N=500 and pulls ahead as N grows, reaching ~4× at N=5000:

TypeNApple (μs)SuiteSparse (μs)Speedup
Float645001,4881,5131.02×
Float642,00038,32278,2262.04×
Float645,000258,3571,042,9054.04×
Float322,00030,55051,2371.68×
Float325,000185,574613,5173.31×
Benchmark environment

SparseArrays.jl v1.12.0 (SuiteSparse 7.8.3). Matrices have density 0.01. Source: bench_sparse.jl.

Benchmark limitations

These benchmarks use random sparse matrices (sprandn) which lack the structure found in real-world problems (e.g., banded, block-diagonal, or mesh-derived sparsity patterns). The matrix sizes tested (N up to 5,000–50,000) are also modest by sparse solver standards. Performance on structured problems from applications like FEM, circuit simulation, or graph analysis may differ significantly.

FFT

FFT performance comparing Apple vDSP against FFTW, both single-threaded. With pre-planned transforms (FFTW.MEASURE plans) FFTW leads across sizes and dimensions — typically 1.5–2.4× for 1D and up to ~14× for large 2D Float64 grids — with vDSP reaching parity only for Float32 at the largest 1D sizes. For no-plan convenience calls, however, the picture changed with the FFT setup cache: AppleAccelerate.fft(x) reuses a cached setup after the first call at each size, and now beats FFTW's unplanned fft(x) at small sizes and wins or ties for Float32 at every size tested (see the last table). vDSP's FFT also remains useful to avoid an FFTW dependency or when Accelerate is already loaded. Source: bench_fft.jl.

Complex 1D FFT (pre-planned)

FFTW leads for 1D complex transforms, typically by 1.6–2.4×; vDSP reaches parity for Float32 at some large sizes:

TypeNvDSP (μs)FFTW (μs)Speedup
ComplexF641,0244.62.02.36× slower
ComplexF644,09619.59.72.0× slower
ComplexF6465,5366112632.32× slower
ComplexF641,048,57613,4238,2491.63× slower
ComplexF321,0242.21.21.79× slower
ComplexF324,09610.94.62.36× slower
ComplexF3265,5361851641.12× slower
ComplexF32262,1446536751.03×
ComplexF321,048,5767,1435,8501.22× slower

Real FFT (pre-planned)

FFTW leads for real transforms too, though vDSP closes to near parity for Float32 at large sizes:

TypeNvDSP (μs)FFTW (μs)Speedup
Float641,0243.31.03.16× slower
Float6465,5362411281.87× slower
Float324,0966.24.11.51× slower
Float3265,53693711.3× slower
Float32262,1443693551.04× slower

Complex 2D FFT (pre-planned)

FFTW is substantially faster for 2D complex transforms — up to ~14× for large Float64 grids — so prefer FFTW when 2D FFTs dominate:

TypeSizevDSP (μs)FFTW (μs)Speedup
ComplexF6464×6432.57.94.12× slower
ComplexF64256×2563,25523114.06× slower
ComplexF3264×6413.64.72.92× slower
ComplexF32256×2562281241.84× slower

Real 2D FFT (pre-planned)

The new 2D real FFT (rfft(A::Matrix)) follows the same pattern — FFTW leads by roughly 2–3.5× at moderate sizes, more for large Float64 grids:

TypeSizevDSP (μs)FFTW (μs)Speedup
Float6464×6417.35.03.5× slower
Float64128×12869.423.22.99× slower
Float64256×2561,18111010.74× slower
Float3264×647.03.81.86× slower
Float32128×12830.415.61.95× slower
Float32256×256156692.25× slower

Batched 1D FFT (columns of a matrix)

The new fft(A, 1) transforms each column via vDSP_fftm_*. FFTW's planned batched transforms lead by 2–4.4×:

TypeSizevDSP (μs)FFTW (μs)Speedup
ComplexF64256×6457.423.02.5× slower
ComplexF641024×642571242.07× slower
ComplexF644096×162941521.94× slower
ComplexF32256×6450.611.44.43× slower
ComplexF321024×64165592.77× slower
ComplexF324096×16181802.25× slower

No-plan fft(x) — cached setups

This table compares the convenience entry points: AppleAccelerate.fft(x) vs FFTW.fft(x), neither given a pre-built plan. AppleAccelerate reuses a cached vDSP setup after the first call at each size (FFTW builds an ESTIMATE plan per call), so for the common "just call fft" pattern vDSP now leads at small sizes and wins or ties for Float32 at every size tested:

TypeNvDSP (μs)FFTW (μs)Speedup
ComplexF641,0243.88.32.16×
ComplexF6465,5364652631.77× slower
ComplexF641,048,57613,5318,8831.52× slower
ComplexF321,0242.34.21.79×
ComplexF3265,5361451461.01×
ComplexF321,048,5764,7045,4031.15×
Benchmark environment

FFTW.jl v1.10.0. Pre-planned tables use FFTW.MEASURE plans on both sides; the no-plan table calls the convenience functions directly. Source: bench_fft.jl.