Repository navigation
Reduce copies when reading files in pyio, match behavior of _io #129005
Description
Activity
- addedtype-featureA feature request or enhancementA feature request or enhancement
on Jan 18, 2025 I took a tangent and looked at the code complexity of adding
os.readintovs. the conditionals aroundos.readvnot existing on some platforms + needing to pass a sequence of buffers toos.readv._pyioUsingos.readvwith conditional: https://github.com/python/cpython/pull/129006/files- Adding
os.readinto,_pyiousing it: cmaloney@52c6013
Happy to work on either course, slight preference for
os.readintoas it feels like by adding one function removes a lot of new conditional code to make these cases more efficient. Code wise, with the Buffer Protocol, the code to me gets quite a bit easier to follow. Not sure it's worth another public OS api to maintain though.Know moving to a full PR for os.readinto would need to add tests and news, just focusing on "adding os.readinto" vs. "using os.readv".
cc: @tomasr8 , @vstinner , @gpshead (reviewers where os.read vs. _Py_read / readinto has come up).
Can you please open a separated issue for
os.readinto()? This function looks useful and simple.Reacted by Cody Maloney- addedperformancePerformance or resource usagePerformance or resource usagestdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directory
on Jan 23, 2025 - added a commit that references this issue
on Jan 28, 2025 39 remaining items
- added a commit that references this issue
on Nov 13, 2025 Reopening as _pyio.BufferedIO layer still can cause an unnecessary copy with its
buf[pos:] + chunk; small patch for that shortly._pyionow has 5 allocations for reading in full which can be retrieved with thetracemalloccode in the main issue post:# .take_bytes() retrieves the bytes from the `.readall()` bytearray ${WORKDIR}/cpython/Lib/_pyio.py:1741: size=8944 B, count=1, average=8944 B # os.fstat allocates a stat result ${WORKDIR}/cpython/Lib/_pyio.py:1602: size=476 B, count=7, average=68 B # Allocate the BufferedReader ${WORKDIR}/cpython/Lib/_pyio.py:258: size=328 B, count=1, average=328 B # Allocate the FileIO ${WORKDIR}/cpython/Lib/_pyio.py:232: size=328 B, count=1, average=328 B # Allocate the BufferedReader Lock ${WORKDIR}/cpython/Lib/_pyio.py:1055: size=56 B, count=1, average=56 B
the
b"" + b"data"case inBufferedReader.readis optimized inbytes_concatso does not copy. Could add a branch to avoid the bytes slicing + add call, but that doesn't significantly change the runtime in my testing so rather leave the code as is for now._pyio.BufferedReaderis using abytesas its buffer currently and I'd like to move that to a list of buffers (which could be bytes, memoryview, or bytearray) but that is out of scope for this issue. That's the current path I'm exploring in my more general buffered rework which I'm prototyping in_pyio._pyio.BufferedReader is using a bytes as its buffer currently
If it's of any help. I've experimented with that for the C buffering code from
readall()which is bugged, it's reading the file in tiny chunks, increasing the buffer few KB at a time then 12.5% at a time after some threshold.
#131052Using a
list of bytesVSbytesmade no improvement because the underlying C code is reallocating the buffer in place.
The reallocation is able to resize the buffer in place in microsecond. (I've only tested on Linux).
The hundred calls to gradually resize the buffer to readall a large file file actually takes no time, it doesn't copy any data. I was very surprised.Reacted by Cody Maloneyre: C
readall(), #140139 should help a little :). A good general grow non-linearly readall loop is one of the things I've been trying to find... But so far the right easy to use and understand abstraction has been out of my grasp.Here I was mainly focused on "
_pyiois much slower than_iofor some cases" and found these copies taking up a significant portion of the time in the largefile tests. Now within 15% which doesn't mean both can't be faster, just there's less of a delta. I'm using_pyioas a testing ground for other structural changes toiowhich should hopefully reduce I/O copies overall.Reacted by morotti
Feature or enhancement
Proposal:
Currently
_pyiouses ~2x as much memory to read all data from a file compared to _io. This is because it makes more than one copy of the data.Details from test_fileio run
Plan:
os.readv()os.readinto()to do readinto like C_Py_readused by_iodoes.os.read()can't take a buffer to use. This aligns behavior between_io.FileIO.readalland_pyio.FileIO.readall.os.readvworks well today and takes a caller allocated buffer rather than needing to add a newosAPI.readv(2)mirrors the behavior and errors ofread(2), so this should keep the same end behavior._pyio.BufferedIOto not force a copy of the buffer for readall when its internal buffer is empty. Currently it always slices its internal buffer then adds the result of_pyio.FileIO.readallto it.For iterating, I'm using a small tracemalloc script to find where copies are:
Loose Ends
os.readvseems to be well supported but is currently guarded by a configure check. I'd like to just make pyio requirereadv, but can do conditional code if needed. If makingreadvnon-optional generally is feasible, happy to work on that.os.readvis not supported on WASI, so need to add conditional code.Has 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
Linked PRs
_pyio.FileIO.readall()#129496