Repository navigation
Conversation
395c279 to
002be51
Compare
Look up the file's write() method once per print() call, instead of once
for each argument, separator and line ending, and pass the cached ' ' and
'\n' strings for the default sep and end directly, instead of converting
them from C strings with PyFile_WriteString(). write() is called exactly
as before.
print() is up to 1.66x faster in pyperf benchmarks, 1.20x geometric
mean; for example, print('a', 1, 2.5) to a file goes from 996 ns to
687 ns.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129ST3GmU89AvSnUNSXu2Yi
002be51 to
abfa80d
Compare
|
(force pushed to fixup author line and make cla bot happy) |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0129ST3GmU89AvSnUNSXu2Yi
|
How about concatenating everything and making a single |
|
I started with that but then found it didn't produce uniform speedup: Copying strings of various sizes into a PyUnicodeWriter instead of doing the "pending" list that TextIOWrapper does would slowdown for some string sizes. It also very significantly changes behavior if a In the longer term I'm hoping to add what I call a "Coherent" print; everything in one print statement gets |
I was surprised to find that |
|
I tried the Benchmark scriptimport pyperf
SETUP = """
import io, os, tempfile
devnull = open(os.devnull, 'w')
sio = io.StringIO()
disk = tempfile.TemporaryFile('w')
ten_ints = tuple(range(10))
big = 'x' * 2**20
big_list = list(range(1000))
"""
CASES = {
"one_str[devnull]": "print('hello world', file=devnull)",
"one_str[sio]": "print('hello world', file=sio)",
"three_args[devnull]": "print('a', 1, 2.5, file=devnull)",
"ten_ints[devnull]": "print(*ten_ints, file=devnull)",
"empty[devnull]": "print(file=devnull)",
"str_1mib[devnull]": "print(big, file=devnull)",
"list_1000[devnull]": "print(big_list, file=devnull)",
"flush[disk]": "print('hello world', file=disk, flush=True)",
}
runner = pyperf.Runner()
for name, stmt in CASES.items():
runner.timeit(name, stmt, SETUP)./python-main bench_print.py -p 10 -o main.json
./python-pr bench_print.py -p 10 -o pr.json
./python-callmethod bench_print.py -p 10 -o callmethod.json
python -m pyperf compare_to main.json pr.json callmethod.json --table
(pyperf, I think it's worthwhile to optimize the Part of what I like for this particular refactoring is that it simplifies the amount of conditional code inside |
Wouldn't combining that improvement with an optimization of
I don't think the benefits are significant enough to justify the additional complexity. |
A single I wanted to validate that so built a microbenchmark just focused on doing more and more arguments. Each argument gets a separator printed as well which doubles the difference. Timing benchmarkimport pyperf
runner = pyperf.Runner()
for n in range(1, 21):
runner.timeit(
f"print_{n:02d}_args",
f"print({"'x', " * n}file=devnull)",
"import os; devnull = open(os.devnull, 'w')",
)./python-pr bench_print_nargs.py -p 10 -o pr.json
./python-callmethod bench_print_nargs.py -p 10 -o callmethod.json
python -m pyperf compare_to pr.json callmethod.json --table
pyperf, |
|
Updated with #160803 and re-measured. This is still slightly slower for zero args, faster for one or more.
Benchmark scriptimport pyperf
runner = pyperf.Runner()
for name, stmt in {
"0_args": "print(file=devnull)",
"1_arg": "print('hello world', file=devnull)",
"3_args": "print('a', 1, 2.5, file=devnull)",
"10_args": "print(*ten_ints, file=devnull)",
}.items():
runner.timeit(
name, stmt,
"import os; devnull = open(os.devnull, 'w'); ten_ints = tuple(range(10))",
)./python-main bench_print.py -p 10 -o main.json
./python-pr bench_print.py -p 10 -o pr.json
python -m pyperf compare_to main.json pr.json --table |
file.writeonce perprint()call, instead of once for each argument, separator and line ending.' 'and'\n'strings for the defaultsepandenddirectly, instead of converting them from C strings withPyFile_WriteString(), which runsstrlen()and the UTF-8 decoder on every call to get back the same cached string.write()receives the same calls as before. Benchmarks are in the issue: 1.20x faster geometric mean across 8 cases, 5 faster (1.12x to 1.66x) and 3 not significant.Developed with the help of AI: Claude Opus 5.5.
print()by looking upfile.writeonce per call #160725