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