Skip to content

Commit a6d446e

Browse files
committed
Merge branch 'cmaloney/readall_faster' into cmaloney/pyio_merged
2 parents 4c65c86 + fa9ac6a commit a6d446e

File tree

2 files changed

+40
-25
lines changed

2 files changed

+40
-25
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Reduce the number of system calls invoked when reading a whole file (ex. ``open('a.txt').read()``). For a sample program that reads the contents of the 400+ ``.rst`` files in the cpython repository ``Doc`` folder, there is a over 10% reduction in system call count.

Modules/_io/fileio.c

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@
5454
# define SMALLCHUNK BUFSIZ
5555
#endif
5656

57+
/* Size at which a buffer is considered "large" and behavior should change to
58+
avoid excessive memory allocation */
59+
#define LARGE_BUFFER_CUTOFF_SIZE 65536
5760

5861
/*[clinic input]
5962
module _io
@@ -72,6 +75,7 @@ typedef struct {
7275
unsigned int closefd : 1;
7376
char finalizing;
7477
unsigned int blksize;
78+
Py_off_t size_estimated;
7579
PyObject *weakreflist;
7680
PyObject *dict;
7781
} fileio;
@@ -196,6 +200,7 @@ fileio_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
196200
self->appending = 0;
197201
self->seekable = -1;
198202
self->blksize = 0;
203+
self->size_estimated = -1;
199204
self->closefd = 1;
200205
self->weakreflist = NULL;
201206
}
@@ -482,6 +487,9 @@ _io_FileIO___init___impl(fileio *self, PyObject *nameobj, const char *mode,
482487
if (fdfstat.st_blksize > 1)
483488
self->blksize = fdfstat.st_blksize;
484489
#endif /* HAVE_STRUCT_STAT_ST_BLKSIZE */
490+
if (fdfstat.st_size < PY_SSIZE_T_MAX) {
491+
self->size_estimated = (Py_off_t)fdfstat.st_size;
492+
}
485493
}
486494

487495
#if defined(MS_WINDOWS) || defined(__CYGWIN__)
@@ -684,7 +692,7 @@ new_buffersize(fileio *self, size_t currentsize)
684692
giving us amortized linear-time behavior. For bigger sizes, use a
685693
less-than-double growth factor to avoid excessive allocation. */
686694
assert(currentsize <= PY_SSIZE_T_MAX);
687-
if (currentsize > 65536)
695+
if (currentsize > LARGE_BUFFER_CUTOFF_SIZE)
688696
addend = currentsize >> 3;
689697
else
690698
addend = 256 + currentsize;
@@ -707,41 +715,48 @@ static PyObject *
707715
_io_FileIO_readall_impl(fileio *self)
708716
/*[clinic end generated code: output=faa0292b213b4022 input=dbdc137f55602834]*/
709717
{
710-
struct _Py_stat_struct status;
711718
Py_off_t pos, end;
712719
PyObject *result;
713720
Py_ssize_t bytes_read = 0;
714721
Py_ssize_t n;
715722
size_t bufsize;
716-
int fstat_result;
717723

718-
if (self->fd < 0)
724+
if (self->fd < 0) {
719725
return err_closed();
726+
}
720727

721-
Py_BEGIN_ALLOW_THREADS
722-
_Py_BEGIN_SUPPRESS_IPH
728+
end = self->size_estimated;
729+
if (end <= 0) {
730+
/* Use a default size and resize as needed. */
731+
bufsize = SMALLCHUNK;
732+
}
733+
else {
734+
/* This is probably a real file, so we try to allocate a
735+
buffer one byte larger than the rest of the file. If the
736+
calculation is right then we should get EOF without having
737+
to enlarge the buffer. */
738+
bufsize = (size_t)(end) + 1;
739+
740+
/* While a lot of code does open().read() to get the whole contents
741+
of a file it is possible a caller seeks/reads a ways into the file
742+
then calls readall() to get the rest, which would result in allocating
743+
more than required. Guard against that for larger files where we expect
744+
the I/O time to dominate anyways while keeping small files fast. */
745+
if (bufsize > LARGE_BUFFER_CUTOFF_SIZE) {
746+
Py_BEGIN_ALLOW_THREADS
747+
_Py_BEGIN_SUPPRESS_IPH
723748
#ifdef MS_WINDOWS
724-
pos = _lseeki64(self->fd, 0L, SEEK_CUR);
749+
pos = _lseeki64(self->fd, 0L, SEEK_CUR);
725750
#else
726-
pos = lseek(self->fd, 0L, SEEK_CUR);
751+
pos = lseek(self->fd, 0L, SEEK_CUR);
727752
#endif
728-
_Py_END_SUPPRESS_IPH
729-
fstat_result = _Py_fstat_noraise(self->fd, &status);
730-
Py_END_ALLOW_THREADS
731-
732-
if (fstat_result == 0)
733-
end = status.st_size;
734-
else
735-
end = (Py_off_t)-1;
753+
_Py_END_SUPPRESS_IPH
754+
Py_END_ALLOW_THREADS
736755

737-
if (end > 0 && end >= pos && pos >= 0 && end - pos < PY_SSIZE_T_MAX) {
738-
/* This is probably a real file, so we try to allocate a
739-
buffer one byte larger than the rest of the file. If the
740-
calculation is right then we should get EOF without having
741-
to enlarge the buffer. */
742-
bufsize = (size_t)(end - pos + 1);
743-
} else {
744-
bufsize = SMALLCHUNK;
756+
if (end >= pos && pos >= 0 && end - pos < PY_SSIZE_T_MAX) {
757+
bufsize = bufsize - Py_SAFE_DOWNCAST(pos, Py_off_t, size_t);
758+
}
759+
}
745760
}
746761

747762
result = PyBytes_FromStringAndSize(NULL, bufsize);
@@ -783,7 +798,6 @@ _io_FileIO_readall_impl(fileio *self)
783798
return NULL;
784799
}
785800
bytes_read += n;
786-
pos += n;
787801
}
788802

789803
if (PyBytes_GET_SIZE(result) > bytes_read) {

0 commit comments

Comments
 (0)