Benchmark wav¶
This example measuers the performance of loading WAV audio.
It compares three different approaches for loading WAV files:
spdl.io.load_wav(): Fast native WAV parser optimized for simple PCM formatsspdl.io.load_audio(): General-purpose audio loader using FFmpeg backendsoundfile(libsndfile): Popular third-party audio I/O library
The benchmark suite evaluates performance across multiple dimensions:
Various audio configurations (sample rates, channels, bit depths, durations)
Different thread counts (1, 2, 4, 8, 16) to measure parallel scaling
Statistical analysis with 95% confidence intervals using Student’s t-distribution
Queries per second (QPS) as the primary performance metric
Example
$ numactl --membind 0 --cpubind 0 python benchmark_wav.py --output wav_benchmark_results.csv
# Plot results
$ python benchmark_wav_plot.py --input wav_benchmark_results.csv --output wav_benchmark_plot.png
# Plot results without load_wav
$ python benchmark_wav_plot.py --input wav_benchmark_results.csv --output wav_benchmark_plot_2.png --filter '3. spdl.io.load_wav'
Result
The following plot shows the QPS (measured by the number of files processed) of each functions with different audio durations.
The spdl.io.load_wav() is a lot faster than the others, because all it
does is reinterpret the input byte string as array.
It shows the same performance for audio with longer duration.
And since parsing WAV is instant, the spdl.io.load_wav function spends more time on creation of NumPy Array. It needs to acquire the GIL, thus the performance does not scale in multi-threading. (This performance pattern of this function is pretty same as the spdl.io.load_npz.)
The following is the same plot without load_wav.
libsoundfile has to process data iteratively (using io.BytesIO) because
it does not support directly loading from byte string, so it takes longer to process
longer audio data.
The performance trend (single thread being the fastest) suggests that
it does not release the GIL majority of the time.
The spdl.io.load_audio() function (the generic FFmpeg-based implementation) does
a lot of work so its overall performance is not as good,
but it scales in multi-threading as it releases the GIL almost entirely.
Free-threaded Python
The cases above where multi-threading does not help are caused by contention on
the GIL — most visibly soundfile, which holds the GIL for most of its work,
so a single thread ends up being the fastest. This contention disappears on a
free-threaded (no-GIL) build of Python (e.g. 3.14t). The following is the
same benchmark run on 3.14t.
Without the GIL, soundfile now scales with the number of threads instead of
slowing down, improving its throughput at 16 threads by more than an order of
magnitude. spdl.io.load_audio(), which already releases the GIL, is
essentially unchanged.
The numbers from both runs are tabulated below.
Soundfile
Duration |
1s |
10s |
60s |
|||
|---|---|---|---|---|---|---|
Build |
3.14 |
3.14t |
3.14 |
3.14t |
3.14 |
3.14t |
1 |
9,904 |
11,910 |
5,864 |
6,500 |
1,869 |
2,054 |
2 |
5,481 |
19,120 |
4,188 |
10,645 |
1,481 |
3,599 |
4 |
3,142 |
30,447 |
2,491 |
16,438 |
1,201 |
6,280 |
8 |
2,375 |
45,220 |
1,993 |
25,208 |
1,163 |
10,725 |
16 |
2,643 |
54,900 |
2,121 |
40,948 |
1,017 |
14,103 |
spdl.io.load_audio
Duration |
1s |
10s |
60s |
|||
|---|---|---|---|---|---|---|
Build |
3.14 |
3.14t |
3.14 |
3.14t |
3.14 |
3.14t |
1 |
116 |
119 |
43 |
45 |
38 |
38 |
2 |
258 |
271 |
85 |
88 |
74 |
75 |
4 |
512 |
511 |
168 |
176 |
144 |
152 |
8 |
1,011 |
1,035 |
337 |
352 |
286 |
296 |
16 |
1,915 |
2,012 |
652 |
698 |
538 |
574 |
spdl.io.load_wav
Duration |
1s |
10s |
60s |
|||
|---|---|---|---|---|---|---|
Build |
3.14 |
3.14t |
3.14 |
3.14t |
3.14 |
3.14t |
1 |
51,585 |
79,572 |
47,905 |
107,119 |
49,404 |
76,757 |
2 |
49,882 |
98,500 |
49,057 |
97,641 |
45,787 |
98,364 |
4 |
51,084 |
87,790 |
49,228 |
91,243 |
50,026 |
94,748 |
8 |
48,710 |
77,335 |
49,124 |
73,603 |
47,977 |
76,574 |
16 |
38,366 |
65,094 |
37,587 |
62,512 |
38,210 |
63,498 |
Source¶
Source
Click here to see the source.
1# Copyright (c) Meta Platforms, Inc. and affiliates.
2# All rights reserved.
3#
4# This source code is licensed under the BSD-style license found in the
5# LICENSE file in the root directory of this source tree.
6
7
8"""This example measuers the performance of loading WAV audio.
9
10It compares three different approaches for loading WAV files:
11
12- :py:func:`spdl.io.load_wav`: Fast native WAV parser optimized for simple PCM formats
13- :py:func:`spdl.io.load_audio`: General-purpose audio loader using FFmpeg backend
14- ``soundfile`` (``libsndfile``): Popular third-party audio I/O library
15
16The benchmark suite evaluates performance across multiple dimensions:
17
18- Various audio configurations (sample rates, channels, bit depths, durations)
19- Different thread counts (1, 2, 4, 8, 16) to measure parallel scaling
20- Statistical analysis with 95% confidence intervals using Student's t-distribution
21- Queries per second (QPS) as the primary performance metric
22
23**Example**
24
25.. code-block:: shell
26
27 $ numactl --membind 0 --cpubind 0 python benchmark_wav.py --output wav_benchmark_results.csv
28 # Plot results
29 $ python benchmark_wav_plot.py --input wav_benchmark_results.csv --output wav_benchmark_plot.png
30 # Plot results without load_wav
31 $ python benchmark_wav_plot.py --input wav_benchmark_results.csv --output wav_benchmark_plot_2.png --filter '3. spdl.io.load_wav'
32
33**Result**
34
35The following plot shows the QPS (measured by the number of files processed) of each
36functions with different audio durations.
37
38.. image:: ../../_static/data/example-benchmark-wav.webp
39
40
41The :py:func:`spdl.io.load_wav` is a lot faster than the others, because all it
42does is reinterpret the input byte string as array.
43It shows the same performance for audio with longer duration.
44
45And since parsing WAV is instant, the spdl.io.load_wav function spends more time on
46creation of NumPy Array.
47It needs to acquire the GIL, thus the performance does not scale in multi-threading.
48(This performance pattern of this function is pretty same as the
49:ref:`spdl.io.load_npz <example-benchmark-numpy>`.)
50
51The following is the same plot without ``load_wav``.
52
53.. image:: ../../_static/data/example-benchmark-wav-2.webp
54
55``libsoundfile`` has to process data iteratively (using ``io.BytesIO``) because
56it does not support directly loading from byte string, so it takes longer to process
57longer audio data.
58The performance trend (single thread being the fastest) suggests that
59it does not release the GIL majority of the time.
60
61The :py:func:`spdl.io.load_audio` function (the generic FFmpeg-based implementation) does
62a lot of work so its overall performance is not as good,
63but it scales in multi-threading as it releases the GIL almost entirely.
64
65**Free-threaded Python**
66
67The cases above where multi-threading does not help are caused by contention on
68the GIL — most visibly ``soundfile``, which holds the GIL for most of its work,
69so a single thread ends up being the fastest. This contention disappears on a
70free-threaded (no-GIL) build of Python (e.g. ``3.14t``). The following is the
71same benchmark run on ``3.14t``.
72
73.. image:: ../../_static/data/example-benchmark-wav-freethreading.png
74
75Without the GIL, ``soundfile`` now scales with the number of threads instead of
76slowing down, improving its throughput at 16 threads by more than an order of
77magnitude. :py:func:`spdl.io.load_audio`, which already releases the GIL, is
78essentially unchanged.
79
80The numbers from both runs are tabulated below.
81
82**Soundfile**
83
84+----------+--------+---------+--------+--------+--------+---------+
85| Duration | 1s | 10s | 60s |
86+----------+--------+---------+--------+--------+--------+---------+
87| Build | 3.14 | 3.14t | 3.14 | 3.14t | 3.14 | 3.14t |
88+==========+========+=========+========+========+========+=========+
89| 1 | 9,904 | 11,910 | 5,864 | 6,500 | 1,869 | 2,054 |
90+----------+--------+---------+--------+--------+--------+---------+
91| 2 | 5,481 | 19,120 | 4,188 | 10,645 | 1,481 | 3,599 |
92+----------+--------+---------+--------+--------+--------+---------+
93| 4 | 3,142 | 30,447 | 2,491 | 16,438 | 1,201 | 6,280 |
94+----------+--------+---------+--------+--------+--------+---------+
95| 8 | 2,375 | 45,220 | 1,993 | 25,208 | 1,163 | 10,725 |
96+----------+--------+---------+--------+--------+--------+---------+
97| 16 | 2,643 | 54,900 | 2,121 | 40,948 | 1,017 | 14,103 |
98+----------+--------+---------+--------+--------+--------+---------+
99
100
101**spdl.io.load_audio**
102
103+----------+--------+---------+--------+--------+--------+---------+
104| Duration | 1s | 10s | 60s |
105+----------+--------+---------+--------+--------+--------+---------+
106| Build | 3.14 | 3.14t | 3.14 | 3.14t | 3.14 | 3.14t |
107+==========+========+=========+========+========+========+=========+
108| 1 | 116 | 119 | 43 | 45 | 38 | 38 |
109+----------+--------+---------+--------+--------+--------+---------+
110| 2 | 258 | 271 | 85 | 88 | 74 | 75 |
111+----------+--------+---------+--------+--------+--------+---------+
112| 4 | 512 | 511 | 168 | 176 | 144 | 152 |
113+----------+--------+---------+--------+--------+--------+---------+
114| 8 | 1,011 | 1,035 | 337 | 352 | 286 | 296 |
115+----------+--------+---------+--------+--------+--------+---------+
116| 16 | 1,915 | 2,012 | 652 | 698 | 538 | 574 |
117+----------+--------+---------+--------+--------+--------+---------+
118
119**spdl.io.load_wav**
120
121+----------+--------+--------+--------+---------+--------+--------+
122| Duration | 1s | 10s | 60s |
123+----------+--------+--------+--------+---------+--------+--------+
124| Build | 3.14 | 3.14t | 3.14 | 3.14t | 3.14 | 3.14t |
125+==========+========+========+========+=========+========+========+
126| 1 | 51,585 | 79,572 | 47,905 | 107,119 | 49,404 | 76,757 |
127+----------+--------+--------+--------+---------+--------+--------+
128| 2 | 49,882 | 98,500 | 49,057 | 97,641 | 45,787 | 98,364 |
129+----------+--------+--------+--------+---------+--------+--------+
130| 4 | 51,084 | 87,790 | 49,228 | 91,243 | 50,026 | 94,748 |
131+----------+--------+--------+--------+---------+--------+--------+
132| 8 | 48,710 | 77,335 | 49,124 | 73,603 | 47,977 | 76,574 |
133+----------+--------+--------+--------+---------+--------+--------+
134| 16 | 38,366 | 65,094 | 37,587 | 62,512 | 38,210 | 63,498 |
135+----------+--------+--------+--------+---------+--------+--------+
136
137
138"""
139
140__all__ = [
141 "BenchmarkConfig",
142 "create_wav_data",
143 "load_sf",
144 "load_spdl_audio",
145 "load_spdl_wav",
146 "main",
147]
148
149import argparse
150import io
151import os
152from collections.abc import Callable
153from dataclasses import dataclass
154
155import numpy as np
156import scipy.io.wavfile
157import soundfile as sf
158import spdl.io
159from numpy.typing import NDArray
160
161try:
162 from examples.benchmark_utils import ( # pyre-ignore[21]
163 BenchmarkResult,
164 BenchmarkRunner,
165 ExecutorType,
166 get_default_result_path,
167 save_results_to_csv,
168 )
169except ImportError:
170 from spdl.examples.benchmark_utils import (
171 BenchmarkResult,
172 BenchmarkRunner,
173 ExecutorType,
174 get_default_result_path,
175 save_results_to_csv,
176 )
177
178
179DEFAULT_RESULT_PATH: str = get_default_result_path(__file__)
180
181
182@dataclass(frozen=True)
183class BenchmarkConfig:
184 """BenchmarkConfig()
185
186 Configuration for a single WAV benchmark run.
187
188 Combines both audio file parameters and benchmark execution parameters.
189 """
190
191 function_name: str
192 """Name of the function being tested"""
193
194 function: Callable[[bytes], NDArray]
195 """The actual function to benchmark"""
196
197 sample_rate: int
198 """Audio sample rate in Hz"""
199
200 num_channels: int
201 """Number of audio channels"""
202
203 bits_per_sample: int
204 """Bit depth per sample (16 or 32)"""
205
206 duration_seconds: float
207 """Duration of the audio file in seconds"""
208
209 num_threads: int
210 """Number of concurrent threads"""
211
212 iterations: int
213 """Number of iterations per run"""
214
215 num_runs: int
216 """Number of runs for statistical analysis"""
217
218
219def create_wav_data(
220 sample_rate: int = 44100,
221 num_channels: int = 2,
222 bits_per_sample: int = 16,
223 duration_seconds: float = 1.0,
224) -> tuple[bytes, NDArray]:
225 """Create a WAV file in memory for benchmarking.
226
227 Args:
228 sample_rate: Sample rate in Hz
229 num_channels: Number of audio channels
230 bits_per_sample: Bits per sample (16 or 32)
231 duration_seconds: Duration of audio in seconds
232
233 Returns:
234 Tuple of (WAV file as bytes, audio samples array)
235 """
236 num_samples = int(sample_rate * duration_seconds)
237
238 dtype_map = {
239 16: np.int16,
240 32: np.int32,
241 }
242 dtype = dtype_map[bits_per_sample]
243 max_amplitude = 32767 if bits_per_sample == 16 else 2147483647
244
245 t = np.linspace(0, duration_seconds, num_samples)
246 frequencies = np.asarray(440.0 + np.arange(num_channels) * 110.0)
247 sine_waves = np.sin(2 * np.pi * frequencies[:, np.newaxis] * t)
248 samples = (sine_waves.T * max_amplitude).astype(dtype)
249
250 wav_buffer = io.BytesIO()
251 scipy.io.wavfile.write(wav_buffer, sample_rate, samples)
252 wav_data = wav_buffer.getvalue()
253
254 return wav_data, samples
255
256
257def load_sf(wav_data: bytes) -> NDArray:
258 """Load WAV data using soundfile library.
259
260 Args:
261 wav_data: WAV file data as bytes
262
263 Returns:
264 Audio samples array as int16 numpy array
265 """
266 audio_file = io.BytesIO(wav_data)
267 data, _ = sf.read(audio_file, dtype="int16")
268 return data
269
270
271def load_spdl_audio(wav_data: bytes) -> NDArray:
272 """Load WAV data using :py:func:`spdl.io.load_audio` function.
273
274 Args:
275 wav_data: WAV file data as bytes
276
277 Returns:
278 Audio samples array as numpy array
279 """
280 return spdl.io.to_numpy(spdl.io.load_audio(wav_data, filter_desc=None))
281
282
283def load_spdl_wav(wav_data: bytes) -> NDArray:
284 """Load WAV data using :py:func:`spdl.io.load_wav` function.
285
286 Args:
287 wav_data: WAV file data as bytes
288
289 Returns:
290 Audio samples array as numpy array
291 """
292 return spdl.io.to_numpy(spdl.io.load_wav(wav_data))
293
294
295def _parse_args() -> argparse.Namespace:
296 """Parse command line arguments for the benchmark script.
297
298 Returns:
299 Parsed command line arguments
300 """
301 parser = argparse.ArgumentParser(description="Benchmark WAV loading performance")
302 parser.add_argument(
303 "--output",
304 type=lambda p: os.path.realpath(p),
305 default=DEFAULT_RESULT_PATH,
306 help="Output file path.",
307 )
308 return parser.parse_args()
309
310
311def main() -> None:
312 """Run comprehensive benchmark suite for WAV loading performance.
313
314 Benchmarks multiple configurations of audio files with different durations,
315 comparing spdl.io.load_wav, spdl.io.load_audio, and soundfile libraries
316 across various thread counts (1, 2, 4, 8, 16).
317 """
318 args = _parse_args()
319
320 # Define audio configurations to test
321 audio_configs = [
322 # (sample_rate, num_channels, bits_per_sample, duration_seconds)
323 # (8000, 1, 16, 1.0), # Low quality mono
324 # (16000, 1, 16, 1.0), # Speech quality mono
325 # (48000, 2, 16, 1.0), # High quality stereo
326 # (48000, 8, 16, 1.0), # Multi-channel audio
327 (44100, 2, 16, 1.0), # CD quality stereo
328 (44100, 2, 16, 10.0), #
329 (44100, 2, 16, 60.0), #
330 # (44100, 2, 24, 1.0), # 24-bit audio
331 ]
332
333 thread_counts = [1, 2, 4, 8, 16]
334
335 # Define benchmark function configurations
336 # (function_name, function, iterations_multiplier, num_runs)
337 benchmark_functions = [
338 ("3. spdl.io.load_wav", load_spdl_wav, 100, 100), # Fast but unstable
339 ("2. spdl.io.load_audio", load_spdl_audio, 10, 5), # Slower but stable
340 ("1. soundfile", load_sf, 10, 5), # Slower but stable
341 ]
342
343 results: list[BenchmarkResult[BenchmarkConfig]] = []
344
345 for sample_rate, num_channels, bits_per_sample, duration_seconds in audio_configs:
346 # Create WAV data for this audio configuration
347 wav_data, ref = create_wav_data(
348 sample_rate=sample_rate,
349 num_channels=num_channels,
350 bits_per_sample=bits_per_sample,
351 duration_seconds=duration_seconds,
352 )
353
354 print(
355 f"\n{sample_rate}Hz, {num_channels}ch, {bits_per_sample}bit, {duration_seconds}s"
356 )
357 print(
358 f"Threads,"
359 f"SPDL WAV QPS ({duration_seconds} sec),CI Lower,CI Upper,"
360 f"SPDL Audio QPS ({duration_seconds} sec),CI Lower,CI Upper,"
361 f"soundfile QPS ({duration_seconds} sec),CI Lower,CI Upper"
362 )
363
364 for num_threads in thread_counts:
365 thread_results: list[BenchmarkResult[BenchmarkConfig]] = []
366
367 with BenchmarkRunner(
368 executor_type=ExecutorType.THREAD,
369 num_workers=num_threads,
370 ) as runner:
371 for (
372 function_name,
373 function,
374 iterations_multiplier,
375 num_runs,
376 ) in benchmark_functions:
377 config = BenchmarkConfig(
378 function_name=function_name,
379 function=function,
380 sample_rate=sample_rate,
381 num_channels=num_channels,
382 bits_per_sample=bits_per_sample,
383 duration_seconds=duration_seconds,
384 num_threads=num_threads,
385 iterations=iterations_multiplier * num_threads,
386 num_runs=num_runs,
387 )
388
389 result, output = runner.run(
390 config,
391 lambda fn=function, data=wav_data: fn(data),
392 config.iterations,
393 num_runs=config.num_runs,
394 )
395
396 output_to_validate = output
397 if output_to_validate.ndim == 1:
398 output_to_validate = output_to_validate[:, None]
399 np.testing.assert_array_equal(output_to_validate, ref)
400
401 thread_results.append(result)
402 results.append(result)
403
404 # Print results for this thread count (all 3 benchmarks)
405 spdl_wav_result = thread_results[0]
406 spdl_audio_result = thread_results[1]
407 soundfile_result = thread_results[2]
408 print(
409 f"{num_threads},"
410 f"{spdl_wav_result.qps:.2f},{spdl_wav_result.ci_lower:.2f},{spdl_wav_result.ci_upper:.2f},"
411 f"{spdl_audio_result.qps:.2f},{spdl_audio_result.ci_lower:.2f},{spdl_audio_result.ci_upper:.2f},"
412 f"{soundfile_result.qps:.2f},{soundfile_result.ci_lower:.2f},{soundfile_result.ci_upper:.2f}"
413 )
414
415 save_results_to_csv(results, args.output)
416 print(
417 f"\nBenchmark complete. To generate plots, run:\n"
418 f"python benchmark_wav_plot.py --input {args.output} "
419 f"--output {args.output.replace('.csv', '.png')}"
420 )
421
422
423if __name__ == "__main__":
424 main()
API Reference¶
Functions
- create_wav_data(sample_rate: int = 44100, num_channels: int = 2, bits_per_sample: int = 16, duration_seconds: float = 1.0) tuple[bytes, NDArray][source]¶
Create a WAV file in memory for benchmarking.
- Parameters:
sample_rate – Sample rate in Hz
num_channels – Number of audio channels
bits_per_sample – Bits per sample (16 or 32)
duration_seconds – Duration of audio in seconds
- Returns:
Tuple of (WAV file as bytes, audio samples array)
- load_sf(wav_data: bytes) NDArray[source]¶
Load WAV data using soundfile library.
- Parameters:
wav_data – WAV file data as bytes
- Returns:
Audio samples array as int16 numpy array
- load_spdl_audio(wav_data: bytes) NDArray[source]¶
Load WAV data using
spdl.io.load_audio()function.- Parameters:
wav_data – WAV file data as bytes
- Returns:
Audio samples array as numpy array
- load_spdl_wav(wav_data: bytes) NDArray[source]¶
Load WAV data using
spdl.io.load_wav()function.- Parameters:
wav_data – WAV file data as bytes
- Returns:
Audio samples array as numpy array
- main() None[source]¶
Run comprehensive benchmark suite for WAV loading performance.
Benchmarks multiple configurations of audio files with different durations, comparing spdl.io.load_wav, spdl.io.load_audio, and soundfile libraries across various thread counts (1, 2, 4, 8, 16).
Classes