From a452936d25848bf799d7c76af74c1660d26e3bc3 Mon Sep 17 00:00:00 2001 From: 284km Date: Sat, 7 Nov 2026 13:39:57 +0900 Subject: [PATCH] Read only the requested range of ordinary files --- bench/README.md | 16 +++++++++++ corpus/502_positioned_file_reads.rb | 44 +++++++++++++++++++++++++++++ main.mere | 37 ++++++++++++++++++++++-- 3 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 corpus/502_positioned_file_reads.rb diff --git a/bench/README.md b/bench/README.md index 5a6920b..6d98fc8 100644 --- a/bench/README.md +++ b/bench/README.md @@ -447,3 +447,19 @@ Two runs before this change used 1,549–1,793 MiB. All runs reached the same 117 tests and retained the same five existing failures; this measures resource usage, not full FileUtils compatibility. The CI limits and test inputs are unchanged. + +## Positioned positive-length reads + +Regular `File#read(length)` calls without universal newline conversion now +open the existing path, seek to the logical position, and read at most the +requested byte count in 64 KiB chunks. This avoids a whole-file copy for each +small read. It keeps the existing path-based behavior and does not cache file +contents, so changes between reads remain visible. Device reads and newline +conversion retain their existing path. + +The `file_read_checks.rb` benchmark above also exercises this path through +`File.foreach`. One local O2 comparison against `23ce853`, using Mere +`991f299b` and the default 1 MiB input, reduced the foreach portion from +28.12 seconds to 24.47 seconds with identical output. Other validation ran +concurrently; this is indicative, not an isolated benchmark or a CI duration +claim. Per-line interpreter work still dominates much of this workload. diff --git a/corpus/502_positioned_file_reads.rb b/corpus/502_positioned_file_reads.rb new file mode 100644 index 0000000..a5b21df --- /dev/null +++ b/corpus/502_positioned_file_reads.rb @@ -0,0 +1,44 @@ +require 'tmpdir' + +Dir.mktmpdir do |dir| + path = File.join(dir, 'data') + data = (0..255).to_a.pack('C*') * 600 + File.binwrite(path, data) + [1, 65535, 65536, 65537, 200000].each do |length| + File.open(path, 'rb') do |f| + f.seek(7) + actual = f.read(length) + expected = data.byteslice(7, length) + p [length, actual == expected, actual.encoding, f.pos] + end + end + File.open(path, 'rb') do |f| + f.seek(data.bytesize - 3) + f.ungetbyte('xyz') + p [f.read(0), f.read(2), f.read(10).bytes, f.pos, f.read(1)] + f.seek(data.bytesize + 5) + p [f.read(1), f.pos, f.read(0)] + end + File.open(path, 'r+b') do |f| + p f.read(2).bytes + f.write('XY') + f.seek(1) + p f.read(4).bytes + f.truncate(3) + f.seek(2) + p [f.read(20), f.pos, f.read(1)] + end + File.open(path, 'rb') do |f| + p f.read(1).bytes + File.binwrite(path, 'updated') + p f.read(3) + File.truncate(path, 0) + p [f.read(1), f.read(0)] + end + File.binwrite(path, "first\r\nsecond\rlast\n") + File.open(path, 'rt', universal_newline: true) do |f| + p [f.read(3), f.gets, f.read] + end + File.open('/dev/zero', 'rb') { |f| p f.read(3).bytes } + File.open(File::NULL, 'rb') { |f| p [f.read(1), f.read(0), f.read] } +end diff --git a/main.mere b/main.mere index 1c6386c..6e29cd8 100644 --- a/main.mere +++ b/main.mere @@ -22982,6 +22982,13 @@ and invoke_val_c0 = fn env -> fn world -> fn blk -> fn (recv0: Val) -> fn (name0 // check nor a read satisfied by pushback needs the file's bytes. if n == 0 then mkstr_e "" "ASCII-8BIT" else + (let uc9 = file_univ_code ivars k mode in + let (ranged9, bytes9) = (if n > 0 && uc9 == 0 then file_read_range path pos n else (false, "")) in + if ranged9 then + (if str_len bytes9 == 0 then VNil + else (let _ = map_set ivars (k ++ "@__f_pos") (VInt (pos + str_len bytes9)) in + mkstr_e bytes9 "ASCII-8BIT")) + else (let content = (if file_exists path then read_file path else "") in // ⚠ A FILE WITH NO BYTES MAY BE A DEVICE: /dev/zero reads as the // empty whole-file string and has any number of bytes to give. @@ -22993,7 +23000,6 @@ and invoke_val_c0 = fn env -> fn world -> fn blk -> fn (recv0: Val) -> fn (name0 let got9 = (if fd9 < 0 then 0 else fd_read fd9 p9 (if n > 65536 then 65536 else n)) in if got9 > 0 then mem_to_str p9 got9 else "") else "") in - let uc9 = file_univ_code ivars k mode in if str_len dev9 > 0 then mkstr_e dev9 "ASCII-8BIT" else if uc9 > 0 && (n < 0 || (n > 0 && pos < str_len content)) then (let (t9, e9) = univ_take content (if pos > str_len content then str_len content else pos) n uc9 in @@ -23008,7 +23014,7 @@ and invoke_val_c0 = fn env -> fn world -> fn blk -> fn (recv0: Val) -> fn (name0 else (let stop = (if pos + n > str_len content then str_len content else pos + n) in let _ = map_set ivars (k ++ "@__f_pos") (VInt stop) in - mkstr_e (substring content pos stop) "ASCII-8BIT")) in + mkstr_e (substring content pos stop) "ASCII-8BIT"))) in // what #ungetc / #ungetbyte pushed back (the prelude keeps it in // @__f_unget) comes out first, as ruby's read buffer hands it out let ug9 = (match (if map_has ivars (k ++ "@__f_unget") then map_get ivars (k ++ "@__f_unget") else VNil) with @@ -45604,6 +45610,33 @@ and file_univ_code = fn ivars -> fn (k: str) -> fn (mode: str) -> let e9 = enc_canon_name (if c2 < 0 then rest9 else substring rest9 0 c2) in if str_eq e9 "UTF-16LE" then 2 else if str_eq e9 "UTF-16BE" then 3 else if str_eq e9 "UTF-32LE" then 4 else if str_eq e9 "UTF-32BE" then 5 else 1)) +// File still owns a path and a logical position. Read only the requested +// range of a regular file without moving a descriptor exposed to Ruby. +// Devices, newline conversion, and failures keep the existing read path. +and file_read_range = fn (path: str) -> fn (pos: int) -> fn (count: int) -> + let mode9 = fs_mode path true in + if pos < 0 || mode9 < 0 || bit_and mode9 61440 != 32768 then (false, "") + else + (let fd9 = fd_open path "r" 438 in + if fd9 < 0 then (false, "") + else + (let moved9 = fd_seek fd9 pos 0 in + if moved9 < 0 then (let _ = fd_close fd9 in (false, "")) + else + (let buf9 = strbuf_new () in + let arena9 = tcp_arena () in + let rec read9 = fn (left9: int) -> + if left9 <= 0 then true + else + (let want9 = (if left9 > 65536 then 65536 else left9) in + let got9 = fd_read fd9 arena9 want9 in + if got9 < 0 then false + else if got9 == 0 then true + else (let _ = strbuf_push buf9 (mem_to_str arena9 got9) in + read9 (left9 - got9))) in + let ok9 = read9 count in + let _ = fd_close fd9 in + (ok9, strbuf_to_str buf9)))) and file_write_at = fn world -> fn (id: int) -> fn (piece: str) -> let (_, _, _, iv9) = world in let k9 = str_of_int id in