Skip to content

Reduce copies when reading files in pyio, match behavior of _io #129005

Description

@cmaloney

Feature or enhancement

Proposal:

Currently _pyio uses ~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

$ ./python -m test -M8g -uall test_largefile -m test_large_read -vvv
== CPython 3.14.0a4+ (heads/main-dirty:3829104ab41, Jan 17 2025, 21:40:47) [Clang 19.1.6 ]
== Linux-6.12.9-arch1-1-x86_64-with-glibc2.40 little-endian
== Python build: debug
== cwd: <$HOME>/python/build/build/test_python_worker_32392æ
== CPU count: 32
== encodings: locale=UTF-8 FS=utf-8
== resources: all

Using random seed: 1740056613
0:00:00 load avg: 0.53 Run 1 test sequentially in a single process
0:00:00 load avg: 0.53 [1/1] test_largefile
test_large_read (test.test_largefile.CLargeFileTest.test_large_read) ... 
 ... expected peak memory use: 4.7G
 ... process data size: 2.3G
ok
test_large_read (test.test_largefile.PyLargeFileTest.test_large_read) ... 
 ... expected peak memory use: 4.7G
 ... process data size: 2.3G
 ... process data size: 4.3G
 ... process data size: 4.7G
ok

----------------------------------------------------------------------
Ran 2 tests in 3.711s

OK

== Tests result: SUCCESS ==

1 test OK.

Total duration: 3.7 sec
Total tests: run=2 (filtered)
Total test files: run=1/1 (filtered)
Result: SUCCESS

Plan:

  1. Switch to os.readv() os.readinto() to do readinto like C _Py_read used by _io does. os.read() can't take a buffer to use. This aligns behavior between _io.FileIO.readall and _pyio.FileIO.readall. os.readv works well today and takes a caller allocated buffer rather than needing to add a new os API. readv(2) mirrors the behavior and errors of read(2), so this should keep the same end behavior.
  2. Update _pyio.BufferedIO to 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.readall to it.

For iterating, I'm using a small tracemalloc script to find where copies are:

from _pyio import open

import tracemalloc

with open("README.rst", 'rb') as file:
    tracemalloc.start()
    data = file.read()
    snap = tracemalloc.take_snapshot()


stats = snap.statistics('lineno')
for stat in stats:
    print(stat)

Loose Ends

  • os.readv seems to be well supported but is currently guarded by a configure check. I'd like to just make pyio require readv, but can do conditional code if needed. If making readv non-optional generally is feasible, happy to work on that.
    • os.readv is 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

Activity

  1. added 4 commits that reference this issue on Jan 18, 2025
  2. cmaloney commented on Jan 22, 2025

    @cmaloney
    ContributorAuthor

    I took a tangent and looked at the code complexity of adding os.readinto vs. the conditionals around os.readv not existing on some platforms + needing to pass a sequence of buffers to os.readv.

    1. _pyio Using os.readv with conditional: https://github.com/python/cpython/pull/129006/files
    2. Adding os.readinto, _pyio using it: cmaloney@52c6013

    Happy to work on either course, slight preference for os.readinto as 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).

  3. vstinner commented on Jan 22, 2025

    @vstinner
    Member

    Can you please open a separated issue for os.readinto()? This function looks useful and simple.

  4. added
    performancePerformance or resource usage
    stdlibStandard Library Python modules in the Lib/ directory
    on Jan 23, 2025
  5. added 2 commits that reference this issue on Jan 26, 2025
  6. added a commit that references this issue on Jan 28, 2025
  7. added 3 commits that reference this issue on Jan 29, 2025
  8. 39 remaining items

  9. added 3 commits that reference this issue on Feb 26, 2025
  10. added a commit that references this issue on Nov 13, 2025
  11. added a commit that references this issue on Nov 18, 2025
  12. cmaloney commented on Nov 18, 2025

    @cmaloney
    ContributorAuthor

    Reopening as _pyio.BufferedIO layer still can cause an unnecessary copy with its buf[pos:] + chunk; small patch for that shortly.

  13. cmaloney commented on Nov 19, 2025

    @cmaloney
    ContributorAuthor

    _pyio now has 5 allocations for reading in full which can be retrieved with the tracemalloc code 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 in BufferedReader.read is optimized in bytes_concat so 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.BufferedReader is using a bytes as 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.

  14. morotti commented on Nov 19, 2025

    @morotti
    Contributor

    _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.
    #131052

    Using a list of bytes VS bytes made 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.

  15. cmaloney commented on Nov 19, 2025

    @cmaloney
    ContributorAuthor

    re: 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 "_pyio is much slower than _io for 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 _pyio as a testing ground for other structural changes to io which should hopefully reduce I/O copies overall.

  16. added a commit that references this issue on Dec 6, 2025
  17. added 2 commits that reference this issue on Sep 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    performancePerformance or resource usagestdlibStandard Library Python modules in the Lib/ directorytype-featureA feature request or enhancement

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions