authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-01-14 19:16:09-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-01-15 14:18:20-08:00
log482189843220d4bd21ab4fdb737c20ee57580769
treed5cd417a1ca03a04e51ce7505924c2fe71566780
parentbed7bc37c43afce335610867aedbcd506ade65f4

std.Io.File.MemoryMap API tuning

- remove file_size parameter from MemoryMap.write - remove requirement for mapping length to be aligned - align allocated fallback memory - add unit test for std.Io.Threaded.disable_memory_mapping = true - add unit test for MemoryMap.setLength

6 files changed, 145 insertions(+), 74 deletions(-)

lib/std/Io.zig+1-1
......@@ -658,7 +658,7 @@ pub const VTable = struct {
658658 fileMemoryMapDestroy: *const fn (?*anyopaque, *File.MemoryMap) void,
659659 fileMemoryMapSetLength: *const fn (?*anyopaque, *File.MemoryMap, n: usize) File.MemoryMap.SetLengthError!void,
660660 fileMemoryMapRead: *const fn (?*anyopaque, *File.MemoryMap) File.ReadPositionalError!void,
661 fileMemoryMapWrite: *const fn (?*anyopaque, *File.MemoryMap, file_size: u64) File.WritePositionalError!void,
661 fileMemoryMapWrite: *const fn (?*anyopaque, *File.MemoryMap) File.WritePositionalError!void,
662662
663663 processExecutableOpen: *const fn (?*anyopaque, File.OpenFlags) std.process.OpenExecutableError!File,
664664 processExecutablePath: *const fn (?*anyopaque, buffer: []u8) std.process.ExecutablePathError!usize,
lib/std/Io/File.zig+1-5
......@@ -68,11 +68,7 @@ pub const Stat = struct {
6868 ctime: Io.Timestamp,
6969 /// Smallest chunk length in bytes appropriate for optimal I/O. This will
7070 /// be set to `1` for operating systems or file systems that do not
71 /// recognize this concept. Not always a power of two. When creating a
72 /// `MemoryMap`, the mapping length must be a multiple of this value.
73 ///
74 /// On Windows, this is whichever is larger: PageSize or
75 /// AllocationGranularity.
71 /// recognize this concept. Not always a power of two.
7672 block_size: BlockSize,
7773};
7874
lib/std/Io/File/MemoryMap.zig+15-14
......@@ -13,8 +13,8 @@ file: File,
1313/// Byte index inside `file` where `memory` starts. Page-aligned.
1414offset: u64,
1515/// Memory that may or may not remain consistent with file contents. Use `read`
16/// and `write` to ensure synchronization points. No minimum alignment on the
17/// pointer is guaranteed, but the length is page-aligned.
16/// and `write` to ensure synchronization points. Pointer is page-aligned but
17/// length is not.
1818memory: []u8,
1919/// Tells whether it is memory-mapped or file operations. On Windows this also
2020/// has a section handle.
......@@ -37,11 +37,13 @@ pub const CreateError = error{
3737} || Allocator.Error || File.ReadPositionalError;
3838
3939pub const CreateOptions = struct {
40 /// Size of the mapping, in bytes. If this is longer than the file size, it
41 /// will be filled with zeroes.
40 /// Size of the mapping, in bytes. If this is longer than the file size,
41 /// `memory` beyond the file end will be filled with zeroes and it is
42 /// unspecified whether, after calling `write`, the file length will be
43 /// set to `len` or remain unchanged.
4244 ///
43 /// Asserted to be a multiple of page size which can be obtained via
44 /// `std.heap.pageSize`.
45 /// This value has no minimum alignment requirement, but may gain
46 /// efficiency benefits from being a multiple of `File.Stat.block_size`.
4547 len: usize,
4648 /// When this has read set to false, bytes that are not modified before a
4749 /// sync may have the original file contents, or may be set to zero.
......@@ -81,10 +83,9 @@ pub fn setLength(
8183 mm: *MemoryMap,
8284 io: Io,
8385 /// New size of the mapping, in bytes. If this is longer than the file
84 /// size, it will be filled with zeroes. Asserted to be a multiple of page
85 /// size which can be obtained with `std.heap.pageSize`.
86 /// size, it will be filled with zeroes. No alignment requirement.
8687 new_length: usize,
87) File.SetLengthError!void {
88) SetLengthError!void {
8889 return io.vtable.fileMemoryMapSetLength(io.userdata, mm, new_length);
8990}
9091
......@@ -95,9 +96,9 @@ pub fn read(mm: *MemoryMap, io: Io) File.ReadPositionalError!void {
9596
9697/// Synchronizes the contents of `memory` to `file`.
9798///
98/// Size of the mapping may be longer than the file size, so the `file_size`
99/// argument is used to avoid writing too many bytes. If `file_size` is not
100/// handy, use `File.length` to get it.
101pub fn write(mm: *MemoryMap, io: Io, file_size: u64) File.WritePositionalError!void {
102 return io.vtable.fileMemoryMapWrite(io.userdata, mm, file_size);
99/// If `memory.len` is greater than file size, the bytes beyond the end of the
100/// file may be dropped, or they may be written, extending the size of the
101/// file.
102pub fn write(mm: *MemoryMap, io: Io) File.WritePositionalError!void {
103 return io.vtable.fileMemoryMapWrite(io.userdata, mm);
103104}
lib/std/Io/Threaded.zig+50-48
......@@ -16172,8 +16172,6 @@ fn fileMemoryMapCreate(
1617216172 const offset = options.offset;
1617316173 const len = options.len;
1617416174
16175 assert(std.mem.isAligned(len, std.heap.page_size_min));
16176
1617716175 if (!t.disable_memory_mapping) {
1617816176 if (createFileMap(file, options.protection, offset, options.populate, len)) |result| {
1617916177 return result;
......@@ -16187,12 +16185,13 @@ fn fileMemoryMapCreate(
1618716185 }
1618816186
1618916187 const gpa = t.allocator;
16188 const page_size = std.heap.pageSize();
16189 const alignment: Alignment = .fromByteUnits(page_size);
1619016190 const memory = m: {
16191 const ptr = gpa.rawAlloc(len, .@"1", @returnAddress()) orelse
16192 return error.OutOfMemory;
16191 const ptr = gpa.rawAlloc(len, alignment, @returnAddress()) orelse return error.OutOfMemory;
1619316192 break :m ptr[0..len];
1619416193 };
16195 errdefer gpa.rawFree(memory, .@"1", @returnAddress());
16194 errdefer gpa.rawFree(memory, alignment, @returnAddress());
1619616195
1619716196 if (!options.undefined_contents) try mmSyncRead(file, memory, offset);
1619816197
......@@ -16363,7 +16362,7 @@ fn fileMemoryMapDestroy(userdata: ?*anyopaque, mm: *File.MemoryMap) void {
1636316362 }
1636416363 } else {
1636516364 const gpa = t.allocator;
16366 gpa.rawFree(memory, .@"1", @returnAddress());
16365 gpa.rawFree(memory, .fromByteUnits(std.heap.pageSize()), @returnAddress());
1636716366 }
1636816367 mm.* = undefined;
1636916368}
......@@ -16374,46 +16373,54 @@ fn fileMemoryMapSetLength(
1637416373 new_len: usize,
1637516374) File.MemoryMap.SetLengthError!void {
1637616375 const t: *Threaded = @ptrCast(@alignCast(userdata));
16377 assert(std.mem.isAligned(new_len, std.heap.page_size_min));
16378 if (mm.section) |section| switch (native_os) {
16379 .windows => {
16380 _ = section;
16381 @panic("TODO");
16382 },
16383 .wasi => unreachable,
16384 else => {
16385 const flags: posix.MREMAP = .{ .MAYMOVE = true };
16386 const addr_hint: ?[*]const u8 = null;
16387 const new_memory = while (true) {
16388 const syscall: Syscall = try .start();
16389 const rc = posix.system.mremap(mm.memory.ptr, mm.memory.len, new_len, flags, addr_hint);
16390 syscall.finish();
16391 const err: posix.E = if (builtin.link_libc) e: {
16392 if (rc != std.c.MAP_FAILED) break @as([*]u8, @ptrCast(@alignCast(rc)))[0..new_len];
16393 break :e @enumFromInt(posix.system._errno().*);
16394 } else e: {
16395 const err = posix.errno(rc);
16396 if (err == .SUCCESS) break @as([*]u8, @ptrFromInt(rc))[0..new_len];
16397 break :e err;
16376 const page_size = std.heap.pageSize();
16377 const alignment: Alignment = .fromByteUnits(page_size);
16378
16379 if (mm.section) |section| {
16380 if (alignment.forward(new_len) == alignment.forward(mm.memory.len)) {
16381 mm.memory.len = new_len;
16382 return;
16383 }
16384 switch (native_os) {
16385 .windows => {
16386 _ = section;
16387 @panic("TODO");
16388 },
16389 .wasi => unreachable,
16390 else => {
16391 const flags: posix.MREMAP = .{ .MAYMOVE = true };
16392 const addr_hint: ?[*]const u8 = null;
16393 const new_memory = while (true) {
16394 const syscall: Syscall = try .start();
16395 const rc = posix.system.mremap(mm.memory.ptr, mm.memory.len, new_len, flags, addr_hint);
16396 syscall.finish();
16397 const err: posix.E = if (builtin.link_libc) e: {
16398 if (rc != std.c.MAP_FAILED) break @as([*]u8, @ptrCast(@alignCast(rc)))[0..new_len];
16399 break :e @enumFromInt(posix.system._errno().*);
16400 } else e: {
16401 const err = posix.errno(rc);
16402 if (err == .SUCCESS) break @as([*]u8, @ptrFromInt(rc))[0..new_len];
16403 break :e err;
16404 };
16405 switch (err) {
16406 .SUCCESS => unreachable,
16407 .INTR => continue,
16408 .AGAIN => return error.LockedMemoryLimitExceeded,
16409 .NOMEM => return error.OutOfMemory,
16410 .INVAL => return errnoBug(err),
16411 .FAULT => return errnoBug(err),
16412 else => return posix.unexpectedErrno(err),
16413 }
1639816414 };
16399 switch (err) {
16400 .SUCCESS => unreachable,
16401 .INTR => continue,
16402 .AGAIN => return error.LockedMemoryLimitExceeded,
16403 .NOMEM => return error.OutOfMemory,
16404 .INVAL => return errnoBug(err),
16405 .FAULT => return errnoBug(err),
16406 else => return posix.unexpectedErrno(err),
16407 }
16408 };
16409 mm.memory = new_memory;
16410 },
16415 mm.memory = new_memory;
16416 },
16417 }
1641116418 } else {
1641216419 const gpa = t.allocator;
16413 if (gpa.rawRemap(mm.memory, .@"1", new_len, @returnAddress())) |new_ptr| {
16420 if (gpa.rawRemap(mm.memory, alignment, new_len, @returnAddress())) |new_ptr| {
1641416421 mm.memory = new_ptr[0..new_len];
1641516422 } else {
16416 const new_ptr = gpa.rawAlloc(new_len, .@"1", @returnAddress()) orelse
16423 const new_ptr = gpa.rawAlloc(new_len, alignment, @returnAddress()) orelse
1641716424 return error.OutOfMemory;
1641816425 const copy_len = @min(new_len, mm.memory.len);
1641916426 @memcpy(new_ptr[0..copy_len], mm.memory[0..copy_len]);
......@@ -16429,16 +16436,11 @@ fn fileMemoryMapRead(userdata: ?*anyopaque, mm: *File.MemoryMap) File.ReadPositi
1642916436 return mmSyncRead(mm.file, mm.memory, mm.offset);
1643016437}
1643116438
16432fn fileMemoryMapWrite(
16433 userdata: ?*anyopaque,
16434 mm: *File.MemoryMap,
16435 file_size: u64,
16436) File.WritePositionalError!void {
16439fn fileMemoryMapWrite(userdata: ?*anyopaque, mm: *File.MemoryMap) File.WritePositionalError!void {
1643716440 const t: *Threaded = @ptrCast(@alignCast(userdata));
1643816441 _ = t;
1643916442 if (mm.section != null) return;
16440 const offset = mm.offset;
16441 return mmSyncWrite(mm.file, mm.memory[0..@intCast(file_size - offset)], offset);
16443 return mmSyncWrite(mm.file, mm.memory, mm.offset);
1644216444}
1644316445
1644416446fn mmSyncRead(file: File, memory: []u8, offset: u64) File.ReadPositionalError!void {
lib/std/Io/Threaded/test.zig+57
......@@ -204,3 +204,60 @@ test "cancel blocked read from pipe" {
204204 try io.sleep(.fromMilliseconds(10), .awake);
205205 try future.cancel(io);
206206}
207
208test "memory mapping fallback" {
209 var threaded: std.Io.Threaded = .init(std.testing.allocator, .{
210 .argv0 = .empty,
211 .environ = .empty,
212 .disable_memory_mapping = true,
213 });
214 defer threaded.deinit();
215 const io = threaded.io();
216
217 var tmp = testing.tmpDir(.{});
218 defer tmp.cleanup();
219
220 try tmp.dir.writeFile(io, .{
221 .sub_path = "blah.txt",
222 .data = "this is my data123",
223 });
224
225 {
226 var file = try tmp.dir.openFile(io, "blah.txt", .{ .mode = .read_write });
227 defer file.close(io);
228
229 // The `Io.File.MemoryMap` API does not specify what happens if we supply a
230 // length greater than file size, but this is testing specifically std.Io.Threaded
231 // with disable_memory_mapping = true.
232 var mm = try file.createMemoryMap(io, .{ .len = "this is my data123".len + 3 });
233 defer mm.destroy(io);
234
235 try testing.expectEqualStrings("this is my data123\x00\x00\x00", mm.memory);
236 mm.memory[4] = '9';
237 mm.memory[7] = '9';
238
239 try mm.write(io);
240 }
241
242 var buffer: [100]u8 = undefined;
243 const updated_contents = try tmp.dir.readFile(io, "blah.txt", &buffer);
244 try testing.expectEqualStrings("this9is9my data123\x00\x00\x00", updated_contents);
245
246 {
247 var file = try tmp.dir.openFile(io, "blah.txt", .{ .mode = .read_only });
248 defer file.close(io);
249
250 var mm = try file.createMemoryMap(io, .{
251 .len = "this9is9my".len,
252 .protection = .{ .read = true },
253 });
254 defer mm.destroy(io);
255
256 try testing.expectEqualStrings("this9is9my", mm.memory);
257
258 try mm.setLength(io, "this9is9my data123".len);
259 try mm.read(io);
260
261 try testing.expectEqualStrings("this9is9my data123", mm.memory);
262 }
263}
lib/std/Io/test.zig+21-6
......@@ -608,20 +608,35 @@ test "memory mapping" {
608608 var file = try tmp.dir.openFile(io, "blah.txt", .{ .mode = .read_write });
609609 defer file.close(io);
610610
611 const stat = try file.stat(io);
612 const aligned_len = std.mem.alignForward(usize, @intCast(stat.size), std.heap.pageSize());
613
614 var mm = try file.createMemoryMap(io, .{ .len = aligned_len });
611 var mm = try file.createMemoryMap(io, .{ .len = "this is my data123".len });
615612 defer mm.destroy(io);
616613
617 try expectEqualStrings("this is my data123", std.mem.sliceTo(mm.memory, 0));
614 try expectEqualStrings("this is my data123", mm.memory);
618615 mm.memory[4] = '9';
619616 mm.memory[7] = '9';
620617
621 try mm.write(io, stat.size);
618 try mm.write(io);
622619 }
623620
624621 var buffer: [100]u8 = undefined;
625622 const updated_contents = try tmp.dir.readFile(io, "blah.txt", &buffer);
626623 try expectEqualStrings("this9is9my data123", updated_contents);
624
625 {
626 var file = try tmp.dir.openFile(io, "blah.txt", .{ .mode = .read_only });
627 defer file.close(io);
628
629 var mm = try file.createMemoryMap(io, .{
630 .len = "this9is9my".len,
631 .protection = .{ .read = true },
632 });
633 defer mm.destroy(io);
634
635 try expectEqualStrings("this9is9my", mm.memory);
636
637 try mm.setLength(io, "this9is9my data123".len);
638 try mm.read(io);
639
640 try expectEqualStrings("this9is9my data123", mm.memory);
641 }
627642}