std.Io.Writer.Allocating 消耗了我所有内存

Lobsters Hottest 工具

摘要

一篇博客文章揭示了 Zig 的 std.Io.Writer.Allocating 中存在的内存过度分配错误,原因是 `drain` 函数在每个数据切片上错误地预留了 splat 参数的空间,导致内存意外增长。

<p><a href="https://lobste.rs/s/eelfmn/std_io_writer_allocating_ate_all_my_memory">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/07/30 19:54

# std.Io.Writer.Allocating 吃光了我的内存 来源:https://www.openmymind.net/std-io-writer-allocating-ate-my-memory/ 如果我告诉你下面的代码输出的是 **1665**: ``` var b: std.ArrayList(u8) = try .initCapacity(init.gpa, 1024); try b.appendSlice(init.gpa, "a" ** 1025); std.debug.print("{d}\n", .{b.capacity}); ``` 你能否猜出下面这段代码输出什么? ``` var w: Io.Writer.Allocating = try .initCapacity(init.gpa, 1024); try w.writer.writeAll("a" ** 1025); std.debug.print("{d}\n", .{w.writer.buffer.len}); ``` 和我一样,你可能会惊讶地看到 **3204**。更奇怪的是,如果你将写入操作拆开,会得到一个更合理的 **1668**: ``` var w: Io.Writer.Allocating = try .initCapacity(init.gpa, 1024); try w.writer.writeAll("a" ** 1024); try w.writer.writeAll("a"); std.debug.print("{d}\n", .{w.writer.buffer.len}); ``` 这是怎么回事?看起来是 `std.Io.Writer.Allocating` 的 `drain` 实现中存在一个 bug。`drain` 是 `Writer` 必须实现的一个方法,除了 `self` 外,它还接受两个参数: ``` fn drain(w: *Writer, data: []const []const u8, splat: usize) Error!usize ``` 它接受一个要写入的值列表(以支持向量化 I/O)和一个 "splat" 计数,即 `data` 中最后一个值应重复写入的次数。我认为 `splat` 在压缩场景中尤其有用。以下是 `zstd/Decompress.zig` 中的相关代码行: ``` try w.splatByteAll(d.literal_streams.one[0], len); ``` 其中 `writer.splatByteAll` 最终会调用 `drain`。所以我们对 `drain` 的参数有了一些了解,但为什么它会增长这么多?下面是 `Allocating` 的 `drain` 函数简化版本: ``` fn drain(self: *Allocating, data: []const []const u8, splat: usize) !usize { const pattern = data[data.len - 1]; const splat_len = pattern.len * splat; const start_len = self.writer.end; for (data) |bytes| { try self.ensureUnusedCapacity(bytes.len + splat_len + 1); @memcpy(self.writer.buffer[self.writer.end..][0..bytes.len], bytes); self.writer.end += bytes.len; } // ... } ``` 你能发现问题吗?我没发现,但 Claude 发现了。对于常见的情况 `data.len == 1` 且 `splat == 1`,我们实际上预留了 2 倍的内存:一次给数据本身,一次给 `splat_len`,而它又等于数据本身(代码中称为 `pattern`)。如果我们调用 `drain(&.{ "hello", " " }, 100)`,代码需要为 `"hello"` 预留 5 字节,为 `pattern`(`" ".len * 100`)预留 100 字节。但实现为每个值都预留了 splat 空间,包括 pattern 本身。 `ArrayList` 没有这个问题:它没有 splat。如果你的使用场景很简单,只是追加字节,或许应该坚持使用 `ArrayList`。

相似文章

静态分配,恒定工作

Lobsters Hottest

本文探讨了静态分配策略,以防止释放后使用、类型混淆等内存安全问题,讨论了对象池和代际索引,并介绍了来自TigerStyle的技巧,以在初始化后避免动态内存分配。

尺寸特化内存分配

Hacker News Top

Go 1.27 引入尺寸特化内存分配,适用于80字节及以下的分配,将分配速度提升20-30%,并提升分配密集型代码的整体程序性能最多1%。

你的Rust服务没有内存泄漏——可能是分配器的问题

Lobsters Hottest

本文描述了一次调试过程:一个Rust服务在负载下内存持续居高不下,但并没有真正泄漏,原因是glibc的ptmalloc分配器没有将释放的内存归还给操作系统。文章解释了分配器的行为,并为Rust开发者提供了见解。

2026年分配器的现状 - 六个月后

Lobsters Hottest

本文更新了Rust中自定义分配器的稳定化进展,重点介绍了最近的开发,例如Allocator trait变得dyn兼容,以及解决不健全性问题的努力。