GeezerVison is showing github.com — Improve performance by using reference stealing methods · Issue #150942 · python/cpython
Improve performance by using reference stealing methods #150942DescriptionFeature or enhancement
Proposal:
We can improve performance of cpython by using stealing methods (_PyList_AppendTakeRef, _PyDict_SetItem_Take2, PyTuple_FromPairSteal etc.) instead of the normal ones. Using stealing methods we typically avoid an incref/decref pair and sometimes error handling is simplified. We should only make changes where it makes a performance impact, the code is not more complex and the modules or methods affected are performance critical.
A list of possible cases (there might be more).
| Location |
Module / context |
Helper |
Modules/_csv.c:688 |
csv reader, per field |
_PyList_AppendTakeRef |
Modules/pyexpat.c:472 |
pyexpat StartElement, per attribute |
_PyDict_SetItem_Take2 |
Modules/_elementtree.c:3351 |
ElementTree C accel attrib dict |
_PyDict_SetItem_Take2 |
Modules/_io/bytesio.c:577 |
io.BytesIO.readlines |
_PyList_AppendTakeRef |
Modules/_io/iobase.c:756 |
io.IOBase.readlines (generic) |
_PyList_AppendTakeRef |
Objects/frameobject.c:639 & 658 |
frame f_locals / vars() / tracing |
_PyList_AppendTakeRef |
Modules/_pickle.c:5138 |
pickle Pickler memo copy |
_PyDict_SetItem_Take2 |
Modules/_io/textio.c:2337 & 2343 |
io.TextIOWrapper readline (multi-chunk) |
_PyList_AppendTakeRef |
Modules/_asynciomodule.c:1213 |
asyncio future done-callback compaction |
_PyList_AppendTakeRef |
Update: Most cases already have an open PR, for new ones please open a separate issue.
Linked PRs
Reacted by Sergey Miryanov, Ivy Xu, An Long, Wulian233, Xuanteng Huang, Timofei Ivankov and Seungki Kim Reacted by stonebig Activityomkar-334 commented on Jun 5, 2026 eendebakpt commented on Jun 5, 2026 ContributorAuthor More actions
hi @eendebakpt @StanFromIreland can i work on a module , maybe csv or textio
Sure! Try out, benchmark, test. If benchmarks show significant improvements you can open a PR, if not just report here briefly. omkar-334 commented on Jun 6, 2026 hi @eendebakpt , i've opned a PR for csv reader, since that has 1.08x speedup, matches your PR speedups....
however, the benchmarks for io.BytesIO.readlines show that there is not a significant improvements
| Benchmark |
main |
my branch |
speedup |
bytesio_readlines_1m_8b |
15.15 ms |
15.29 ms |
0.99x |
bytesio_readlines_500k_24b |
7.66 ms |
7.45 ms |
1.03x |
bytesio_readlines_100k_256b |
3.24 ms |
3.20 ms |
1.01x |
bytesio_readlines_2m_0b |
16.33 ms |
16.54 ms |
0.99x |
bytesio_readlines_2m_8b |
32.58 ms |
31.97 ms |
1.02x |
| geomean |
|
|
1.01x |
Benchmark script
"""Micro-benchmark for io.BytesIO.readlines result-list building."""
from __future__ import annotations
import argparse
import io
import json
import statistics
import sys
import time
from pathlib import Path
def make_data(lines: int, payload_size: int) -> bytes:
payload = b"x" * payload_size
return b"".join(payload + b"\n" for _ in range(lines))
def time_readlines(data: bytes) -> float:
t0 = time.perf_counter_ns()
io.BytesIO(data).readlines()
return (time.perf_counter_ns() - t0) / 1e9
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--lines", type=int, default=200_000)
ap.add_argument("--payload-size", type=int, default=24)
ap.add_argument("--trials", type=int, default=15)
ap.add_argument("--warmup", type=int, default=3)
ap.add_argument("--label", default="run")
ap.add_argument("--outdir", default="results/bytesio-readlines")
args = ap.parse_args()
data = make_data(args.lines, args.payload_size)
for _ in range(args.warmup):
time_readlines(data)
trials = [time_readlines(data) for _ in range(args.trials)]
outdir = Path(args.outdir)
outdir.mkdir(parents=True, exist_ok=True)
summary = {
"label": args.label,
"python": sys.executable,
"lines": args.lines,
"payload_size": args.payload_size,
"trials": args.trials,
"warmup": args.warmup,
"input_bytes": len(data),
"seconds": {
"min": min(trials),
"median": statistics.median(trials),
"mean": statistics.fmean(trials),
"stdev": statistics.pstdev(trials),
"all": trials,
},
"lines_per_sec_at_min": args.lines / min(trials),
}
out = outdir / f"bench_{args.label}.json"
out.write_text(json.dumps(summary, indent=2))
s = summary["seconds"]
print(
f"{args.label}: min={s['min']*1000:.2f}ms median={s['median']*1000:.2f}ms "
f"mean={s['mean']*1000:.2f}ms stdev={s['stdev']*1000:.2f}ms "
f"lines/s@min={summary['lines_per_sec_at_min']:.2e}"
)
print(f"wrote {out}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
do you think i should still make a pr for this tangyuan0821 commented on Jun 7, 2026 Happy to try _asynciomodule.c — swap PyList_Append for
_PyList_AppendTakeRef in future_add_done_callback.
I'll benchmark first. tkim602 commented on Aug 21, 2026 Hi, I found an additional candidate in Python/modsupport.c::do_mkdict(), where Py_BuildValue() dictionary construction passes newly owned key/value references to PyDict_SetItem() and then immediately decrefs them. Using _PyDict_SetItem_Take2() avoids those INCREF/DECREF round trips. Targeted Py_BuildValue() dict microbenchmarks showed small but repeatable ~1–5% improvements across the tested cases.
Opened #156199 for this. eendebakpt commented on Aug 22, 2026 ContributorAuthor More actions It seems all of the obvious cases have an open PR, so I am going to close the issue. For any new cases, please open a new issue. StanFromIreland commented on Aug 22, 2026 I think we should keep this issue open however, unless you plan to move all the PRs? added a commit that references this issue on Oct 2, 2026gh-150942: Speed up BytesIO.readlines() using reference-stealing appe… picnixz commented on Oct 2, 2026 Keeping it open because there are still PRs.
MetadataMetadataDevelopmentNo branches or pull requestsIssue actions
|