← 输入框|github.com|刷新
Skip to content

Add io_whole_file benchmark and an opt-in io group - #498

Open
jayzuccarelli wants to merge 1 commit into
python:mainfrom
jayzuccarelli:io-whole-file
Open

jayzuccarelli wants to merge 1 commit into
python:mainfrom
jayzuccarelli:io-whole-file

Conversation

@jayzuccarelli

Copy link
Copy Markdown

First of three PRs for #399, one benchmark each as suggested there. This one adds the shared piece (an io group that is not part of default) together with the first benchmark that uses it.

What it measures

io_whole_file reads and writes a whole file in a single call, which is the pattern gh-120754 optimized:

  • read_bytes_{4k,1m}: open(path, "rb").read() plus Path.read_bytes() per loop
  • write_bytes_{4k,1m}: open(path, "wb").write(data) plus Path.write_bytes(data) per loop
  • read_text_1m / write_text_1m: Path.read_text() / Path.write_text(), utf-8, ASCII content

6 results. 4 KiB is where per-call overhead and the number of system calls dominate; 1 MiB is where buffer allocation and copying dominate. I had an 8 MiB size too, but it scaled linearly from 1 MiB and sits closer to dirty page throttling, so I dropped it to keep the result count small.

Everything runs in a fresh temp dir with a warm page cache and no fsync, so it measures Python's I/O stack and syscall overhead rather than the disk. Writes always create a new file (unlinked outside the timed region): truncating and rewriting an existing file on ext4 triggers writeback on close (auto_da_alloc), which made a 16 MiB rewrite take ~490 ms +- 220 ms instead of ~30 ms. The run records io.DEFAULT_BUFFER_SIZE in the metadata.

Setup allocates and frees one 8 MiB block before anything is measured. On glibc that raises malloc's dynamic mmap/trim thresholds above the 1 MiB working set; without it read_text_1m (which frees two 1 MiB blocks per call) sat right at the trim threshold and, depending on heap history, flipped between ~430 us and ~1.3 ms on the same interpreter because the heap top was handed back to the kernel and faulted in again every call.

Why opt-in

Per the issue, I/O benchmarks shouldn't run by default. The benchmark is tagged io, [group io] picks it up through the tag, and [group default] excludes it by name (a -io group exclusion is silently ignored by the manifest parser, so it has to name the benchmark). pyperformance list is unchanged at 100 benchmarks; -b io selects it.

Sample timings (pyperformance run -b io_whole_file --fast, one untuned Linux box, ext4 /tmp)

result 3.13.13 3.14.4
io_whole_file_read_bytes_4k 31.5 us 25.0 us
io_whole_file_write_bytes_4k 68.2 us 67.6 us
io_whole_file_read_bytes_1m 302 us 330 us
io_whole_file_write_bytes_1m 1.77 ms 1.82 ms
io_whole_file_read_text_1m 443 us 483 us
io_whole_file_write_text_1m 1.07 ms 1.08 ms

How to run

python -m pyperformance run -b io
python -m pyperformance run -b io_whole_file

Tests

  • python -m pyperformance.tests passes (it runs -b all, so the io benchmarks execute there too)
  • pyperformance list unchanged at 100, list -b io is just io_whole_file
  • flake8 with the tox config is clean on the new file
  • No version bump: 1.14.0 is already tagged, so the changelog line goes under a new 1.15.0 heading. Happy to bump __version__ too if you want that in the same PR, as Add bm_base64 covering common base64 module APIs #447 did.

Part of #399.

AI disclosure: I used Claude Code to write the benchmark and docs; I reviewed the code and ran it.

Add an "io" benchmark group, resolved through the "io" tag and excluded
from the default group, so I/O benchmarks only run with "-b io".

io_whole_file reads and writes a whole file in a single call:
open().read()/write() in binary mode plus Path.read_bytes/write_bytes at
4 KiB and 1 MiB, and Path.read_text/write_text at 1 MiB (6 results).

Part of python#399.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant