std.Io.Writer.Allocating 消耗了我所有内存
摘要
一篇博客文章揭示了 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`。
相似文章
zalloc: 在你的 C 代码中使用 Zig 分配器
zalloc 将 C 模块中的 malloc、calloc、realloc 和 free 替换为 Zig 分配器,从而在 C 代码中实现 Zig 风格的内存管理。
静态分配,恒定工作
本文探讨了静态分配策略,以防止释放后使用、类型混淆等内存安全问题,讨论了对象池和代际索引,并介绍了来自TigerStyle的技巧,以在初始化后避免动态内存分配。
尺寸特化内存分配
Go 1.27 引入尺寸特化内存分配,适用于80字节及以下的分配,将分配速度提升20-30%,并提升分配密集型代码的整体程序性能最多1%。
你的Rust服务没有内存泄漏——可能是分配器的问题
本文描述了一次调试过程:一个Rust服务在负载下内存持续居高不下,但并没有真正泄漏,原因是glibc的ptmalloc分配器没有将释放的内存归还给操作系统。文章解释了分配器的行为,并为Rust开发者提供了见解。
2026年分配器的现状 - 六个月后
本文更新了Rust中自定义分配器的稳定化进展,重点介绍了最近的开发,例如Allocator trait变得dyn兼容,以及解决不健全性问题的努力。