Repository navigation
Speed up print() by looking up file.write once per call #160725
Description
Feature or enhancement
print() currently looks up file.write, creating a new bound method, for every argument, separator, and line ending. Currently print('a', 1, 2.5) does six lookups. The default sep and end also go through PyFile_WriteString(), which converts " " and "\n" from C strings with strlen() and the UTF-8 decoder each time, only to return CPython's cached one-character strings. Looking write up once per call and passing the cached strings directly makes print() 1.20x faster (geometric mean), and write() is still called exactly as before.
Measurements
| Benchmark | main |
patched | Time | Instructions |
|---|---|---|---|---|
print('hello world'), /dev/null |
291 ns | 234 ns | 1.25x faster | −18% |
print('hello world'), StringIO |
253 ns | 193 ns | 1.31x faster | −14% |
print('a', 1, 2.5), /dev/null |
996 ns | 687 ns | 1.45x faster | −31% |
print(*range(10)), /dev/null |
2.86 µs | 1.73 µs | 1.66x faster | −39% |
print(), /dev/null |
n.s. | −3% | ||
print('x' * 2**20), /dev/null |
n.s. | |||
print(list(range(1000))), /dev/null |
n.s. | −0.1% | ||
print('hello world', flush=True), disk file |
1.55 µs | 1.38 µs | 1.12x faster |
Time: pyperf, -p 10, release build without PGO, Linux x86_64. n.s. = not significant. Instructions: per print() call, measured with valgrind (cachegrind) and PYTHONHASHSEED=0, with a noise floor of about ±1%.
Behavior change
write is looked up once per call. If an argument's __str__ rebinds file.write, the new method is used from the next print() call on. A file without a write method still raises AttributeError before any argument is converted.
Benchmark
Benchmark script
import 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-patched bench_print.py -p 10 -o patched.json
python -m pyperf compare_to main.json patched.json --tableHas this already been discussed elsewhere?
This is a minor feature, which does not need previous discussion elsewhere
Links to previous discussion of this feature:
No response
Activity
pythongh-160725: Speed up print() by looking up write() once per call
pythongh-160725: Speed up print() by looking up write() once per call
pythongh-160725: Speed up print() by looking up write() once per call