From e45f8293aefbb74cca056b7ff1556529e9332c11 Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Thu, 3 Feb 2022 22:07:44 +0100 Subject: [PATCH 1/4] ext/standard/file: disable the read buffer in file_get_contents() The read buffer is useless here, it only hurts performance. --- ext/standard/file.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ext/standard/file.c b/ext/standard/file.c index 4c31ee0eae661..5c0bf178bbbfd 100644 --- a/ext/standard/file.c +++ b/ext/standard/file.c @@ -566,6 +566,11 @@ PHP_FUNCTION(file_get_contents) RETURN_FALSE; } + /* disabling the read buffer allows doing the whole transfer + in just one read() system call */ + if (php_stream_is(stream, PHP_STREAM_IS_STDIO)) + php_stream_set_option(stream, PHP_STREAM_OPTION_READ_BUFFER, PHP_STREAM_BUFFER_NONE, NULL); + if (offset != 0 && php_stream_seek(stream, offset, ((offset > 0) ? SEEK_SET : SEEK_END)) < 0) { php_error_docref(NULL, E_WARNING, "Failed to seek to position " ZEND_LONG_FMT " in the stream", offset); php_stream_close(stream); From db480d78165982db471a7de052638ee726e7a09c Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Thu, 3 Feb 2022 22:21:52 +0100 Subject: [PATCH 2/4] main/streams/streams: check php_stream_eof() before php_stream_read() This eliminates the last redundant read() system call in every file_get_contents() call. --- main/streams/streams.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/streams/streams.c b/main/streams/streams.c index 8a7e328007215..6d9814953989c 100644 --- a/main/streams/streams.c +++ b/main/streams/streams.c @@ -1517,7 +1517,7 @@ PHPAPI zend_string *_php_stream_copy_to_mem(php_stream *src, size_t maxlen, int ptr = ZSTR_VAL(result); // TODO: Propagate error? - while ((ret = php_stream_read(src, ptr, max_len - len)) > 0){ + while (!php_stream_eof(src) && (ret = php_stream_read(src, ptr, max_len - len)) > 0){ len += ret; if (len + min_room >= max_len) { result = zend_string_extend(result, max_len + step, persistent); From a3df27820b27f72941aa3509cf3d91135c048331 Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Thu, 3 Feb 2022 23:17:22 +0100 Subject: [PATCH 3/4] main/streams/streams: don't buffer large reads If the read buffer is currently empty, don't buffer large reads (larger than chunk_size), because buffering would only hurt performance in such an edge case. --- main/streams/streams.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/main/streams/streams.c b/main/streams/streams.c index 6d9814953989c..1f4af3adfa634 100644 --- a/main/streams/streams.c +++ b/main/streams/streams.c @@ -706,7 +706,11 @@ PHPAPI ssize_t _php_stream_read(php_stream *stream, char *buf, size_t size) break; } - if (!stream->readfilters.head && ((stream->flags & PHP_STREAM_FLAG_NO_BUFFER) || stream->chunk_size == 1)) { + /* if the read buffer is currently empty, don't buffer + * large reads (larger than chunk_size), because + * buffering would only hurt performance in such an + * edge case */ + if (!stream->readfilters.head && ((stream->flags & PHP_STREAM_FLAG_NO_BUFFER) || stream->chunk_size == 1 || (stream->writepos == stream->readpos && size >= stream->chunk_size))) { toread = stream->ops->read(stream, buf, size); if (toread < 0) { /* Report an error if the read failed and we did not read any data From bf7586569de74e4640178bb5766e04e9f02642e0 Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Fri, 4 Feb 2022 15:03:08 +0100 Subject: [PATCH 4/4] main/streams/streams: optimized code path for unfiltered regular files If we know the exact file size already, we can allocate the right buffer size and issue only one read() system call. This eliminates unnecessary buffer allocations (file size plus 8 kB), eliminates buffer reallocations via zend_string_truncate(), and eliminates the last read() system call. Benchmark: echo Hello World >/tmp/hello.txt --EXPECTF-- -Notice: stream_get_contents(): Read of 8192 bytes failed with errno=9 Bad file descriptor in %s on line %d string(0) "" - -Notice: stream_get_contents(): Read of 8192 bytes failed with errno=9 Bad file descriptor in %s on line %d string(0) "" - -Notice: stream_get_contents(): Read of 8192 bytes failed with errno=9 Bad file descriptor in %s on line %d string(0) "" diff --git a/main/streams/streams.c b/main/streams/streams.c index 1f4af3adfa634..f2c599b3f8c2e 100644 --- a/main/streams/streams.c +++ b/main/streams/streams.c @@ -1474,6 +1474,26 @@ PHPAPI zend_string *_php_stream_copy_to_mem(php_stream *src, size_t maxlen, int return ZSTR_EMPTY_ALLOC(); } + if (php_stream_is(src, PHP_STREAM_IS_STDIO) && + src->readfilters.head == NULL && + php_stream_stat(src, &ssbuf) == 0 && +#ifdef S_ISREG + S_ISREG(ssbuf.sb.st_mode) && +#endif + ssbuf.sb.st_size >= 0) { + /* optimized code path for unfiltered regular files: + * if we know the exact size, we can allocate the + * right buffer size and issue only one read() system + * call */ + + if (ssbuf.sb.st_size <= src->position) + return ZSTR_EMPTY_ALLOC(); + + const size_t remaining = ssbuf.sb.st_size - src->position; + if (remaining < maxlen) + maxlen = remaining; + } + if (maxlen == PHP_STREAM_COPY_ALL) { maxlen = 0; }