如果我告诉你这段代码会打印 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 预留 100
字节的空间(" ".len * 100)。但现在的实现为每一个值都预留了
splat 空间,包括 pattern 本身。
ArrayList 不存在这个问题:它没有
splat。如果你的使用场景很简单,只是单纯地追加字节,你可能还是继续使用它为好。
https://www.openmymind.net/std-io-writer-allocating-ate-my-memory/
Zig 中文社区是一个开放的组织,我们致力于推广 Zig 在中文群体中的使用,有多种方式可以参与进来: 1. 供稿,分享自己使用 Zig 的心得 2. 改进 ZigCC 组织下的开源项目 3. 加入微信群、QQ 群、QQ 频道、Telegram 群组、Google Groups 与更多 Zig 爱好者交流