|
| 1 | +--- |
| 2 | +title: "Zig 分配器的应用" |
| 3 | +date: 2024-06-16T12:11:44+0800 |
| 4 | +--- |
| 5 | + |
| 6 | +> 原文地址: <https://www.openmymind.net/Leveraging-Zigs-Allocators/> |
| 7 | +
|
| 8 | +假设我们想为Zig编写一个 [HTTP服务器库](https://github.com/karlseguin/http.zig)。这个库的核心可能是线程池,用于处理请求。以简化的方式来看,它可能类似于: |
| 9 | + |
| 10 | +```zig |
| 11 | +fn run(worker: *Worker) void { |
| 12 | + while (queue.pop()) |conn| { |
| 13 | + const action = worker.route(conn.req.url); |
| 14 | + action(conn.req, conn.res) catch { // TODO: 500 }; |
| 15 | + worker.write(conn.res); |
| 16 | + } |
| 17 | +} |
| 18 | +``` |
| 19 | + |
| 20 | +作为这个库的用户,您可能会编写一些动态内容的操作。如果假设在启动时为服务器提供分配器(Allocator),则可以将此分配器传递给动作: |
| 21 | + |
| 22 | +```zig |
| 23 | +fn run(worker: *Worker) void { |
| 24 | + const allocator = worker.server.allocator; |
| 25 | + while (queue.pop()) |conn| { |
| 26 | + const action = worker.route(conn.req.url); |
| 27 | + action(allocator, conn.req, conn.res) catch { // TODO: 500 }; |
| 28 | + worker.write(conn.res); |
| 29 | + } |
| 30 | +} |
| 31 | +``` |
| 32 | + |
| 33 | +这允许用户编写如下的操作: |
| 34 | + |
| 35 | +```zig |
| 36 | +fn greet(allocator: Allocator, req: *http.Request, res: *http.Response) !void { |
| 37 | + const name = req.query("name") orelse "guest"; |
| 38 | + res.status = 200; |
| 39 | + res.body = try std.fmt.allocPrint(allocator, "Hello {s}", .{name}); |
| 40 | +} |
| 41 | +``` |
| 42 | + |
| 43 | +虽然这是一个正确的方向,但存在明显的问题:分配的问候语从未被释放。我们的`run`函数不能在写回应后就调用`allocator.free(conn.res.body)`,因为在某些情况下,主体可能不需要被释放。我们可以通过使动作必须 `write()` 回应并因此能够`free`它所做的任何分配来结构化API,但这将使得添加一些功能变得不可能,比如支持中间件。 |
| 44 | + |
| 45 | +最佳和最简单的方法是使用 `ArenaAllocator` 。其工作原理很简单:当我们`deinit`时,所有分配都被释放。 |
| 46 | + |
| 47 | +```zig |
| 48 | +fn run(worker: *Worker) void { |
| 49 | + const allocator = worker.server.allocator; |
| 50 | + while (queue.pop()) |conn| { |
| 51 | + var arena = std.heap.ArenaAllocator.init(allocator); |
| 52 | + defer arena.deinit(); |
| 53 | + const action = worker.route(conn.req.url); |
| 54 | + action(arena.allocator(), conn.req, conn.res) catch { // TODO: 500 }; |
| 55 | + worker.write(conn.res); |
| 56 | + } |
| 57 | +} |
| 58 | +``` |
| 59 | + |
| 60 | +`std.mem.Allocator` 是一个 "[接口](https://www.openmymind.net/Zig-Interfaces/)" ,我们的动作无需更改。 `ArenaAllocator` 对HTTP服务器来说是一个很好的选择,因为它们与请求绑定,具有明确/可理解的生命周期,并且相对短暂。虽然有可能滥用它们,但可以说:使用更多! |
| 61 | + |
| 62 | +我们可以更进一步并重用相同的Arena。这可能看起来不太有用,但是请看: |
| 63 | + |
| 64 | +```zig |
| 65 | +fn run(worker: *Worker) void { |
| 66 | + const allocator = worker.server.allocator; |
| 67 | + var arena = std.heap.ArenaAllocator.init(allocator); |
| 68 | + defer arena.deinit(); |
| 69 | + while (queue.pop()) |conn| { |
| 70 | + // 魔法在此处! |
| 71 | + defer _ = arena.reset(.{.retain_with_limit = 8192}); |
| 72 | + const action = worker.route(conn.req.url); |
| 73 | + action(arena.allocator(), conn.req, conn.res) catch { // TODO: 500 }; |
| 74 | + worker.write(conn.res); |
| 75 | + } |
| 76 | +} |
| 77 | +``` |
| 78 | + |
| 79 | +我们将Arena移出了循环,但重要的部分在内部:每个请求后,我们重置了Arena并保留最多8K内存。这意味着对于许多请求,我们无需访问底层分配器(`worker.server.allocator`)。这种方法简化了内存管理。 |
| 80 | + |
| 81 | +现在想象一下,如果我们不能用 `retain_with_limit` 重置 Arena,我们还能进行同样的优化吗?可以,我们可以创建自己的分配器,首先尝试使用固定缓冲区分配器(FixedBufferAllocator),如果分配适配,回退到 Arena 分配器。 |
| 82 | + |
| 83 | +这里是 `FallbackAllocator` 的完整示例: |
| 84 | + |
| 85 | +```zig |
| 86 | +const FallbackAllocator = struct { |
| 87 | + primary: Allocator, |
| 88 | + fallback: Allocator, |
| 89 | + fba: *std.heap.FixedBufferAllocator, |
| 90 | +
|
| 91 | + pub fn allocator(self: *FallbackAllocator) Allocator { |
| 92 | + return .{ |
| 93 | + .ptr = self, |
| 94 | + .vtable = &.{.alloc = alloc, .resize = resize, .free = free}, |
| 95 | + }; |
| 96 | + } |
| 97 | +
|
| 98 | + fn alloc(ctx: *anyopaque, len: usize, ptr_align: u8, ra: usize) ?[*]u8 { |
| 99 | + const self: *FallbackAllocator = @ptrCast(@alignCast(ctx)); |
| 100 | + return self.primary.rawAlloc(len, ptr_align, ra) |
| 101 | + orelse self.fallback.rawAlloc(len, ptr_align, ra); |
| 102 | + } |
| 103 | +
|
| 104 | + fn resize(ctx: *anyopaque, buf: []u8, buf_align: u8, new_len: usize, ra: usize) bool { |
| 105 | + const self: *FallbackAllocator = @ptrCast(@alignCast(ctx)); |
| 106 | + if (self.fba.ownsPtr(buf.ptr)) { |
| 107 | + if (self.primary.rawResize(buf, buf_align, new_len, ra)) { |
| 108 | + return true; |
| 109 | + } |
| 110 | + } |
| 111 | + return self.fallback.rawResize(buf, buf_align, new_len, ra); |
| 112 | + } |
| 113 | +
|
| 114 | + fn free(_: *anyopaque, _: []u8, _: u8, _: usize) void { |
| 115 | + // we noop this since, in our specific case, we know |
| 116 | + // the fallback is an arena, which won't free individual items |
| 117 | + } |
| 118 | +}; |
| 119 | +``` |
| 120 | + |
| 121 | +我们的`alloc`实现首先尝试使用我们定义的"主"分配器进行分配。如果失败,我们会使用"备用"分配器。作为`std.mem.Allocator`接口的一部分,我们需要实现的`resize`方法会确定正在尝试扩展内存的所有者,并然后调用其`rawResize`方法。为了保持代码简单,我在这里省略了`free`方法的具体实现——在这种特定情况下是可以接受的,因为我们计划使用"主"分配器作为`FixedBufferAllocator`,而"备用"分配器则会是`ArenaAllocator`(因此所有释放操作会在arena的`deinit`或`reset`时进行)。 |
| 122 | + |
| 123 | +接下来我们需要改变我们的`run`方法以利用这个新的分配器: |
| 124 | + |
| 125 | +```zig |
| 126 | +fn run(worker: *Worker) void { |
| 127 | + const allocator = worker.server.allocator; // 这是FixedBufferAllocator底层的内存 |
| 128 | + const buf = try allocator.alloc(u8, 8192); // 分配8K字节的内存用于存储数据 |
| 129 | + defer allocator.free(buf); // 完成后释放内存 |
| 130 | +
|
| 131 | + var fba = std.heap.FixedBufferAllocator.init(buf); // 初始化FixedBufferAllocator |
| 132 | +
|
| 133 | + while (queue.pop()) |conn| { |
| 134 | + defer fba.reset(); // 重置FixedBufferAllocator,准备处理下一个请求 |
| 135 | +
|
| 136 | + var arena = std.heap.ArenaAllocator.init(allocator); // 初始化ArenaAllocator用于分配额外内存 |
| 137 | + defer arena.deinit(); |
| 138 | +
|
| 139 | + var fallback = FallbackAllocator{ |
| 140 | + .fba = &fba, |
| 141 | + .primary = fba.allocator(), |
| 142 | + .fallback = arena.allocator(), |
| 143 | + }; // 创建FallbackAllocator,包含FixedBufferAllocator和ArenaAllocator |
| 144 | +
|
| 145 | + const action = worker.route(conn.req.url); // 路由请求到对应的动作处理函数 |
| 146 | + action(fallback.allocator(), conn.req, conn.res) catch { // 处理动作执行中的错误 }; |
| 147 | +
|
| 148 | + worker.write(conn.res); // 写回响应信息给客户端 |
| 149 | + } |
| 150 | +} |
| 151 | +``` |
| 152 | + |
| 153 | +这种方法实现了类似于在`retain_with_limit`中重置arena的功能。我们创建了一个可以重复使用的`FixedBufferAllocator`,用于处理每个请求的8K字节内存需求。由于一个动作可能需要更多的内存,我们仍然需要`ArenaAllocator`来提供额外的空间。通过将`FixedBufferAllocator`和`ArenaAllocator`包裹在我们的`FallbackAllocator`中,我们可以确保任何分配都首先尝试使用(非常快的)`FixedBufferAllocator`,当其空间用尽时,则会切换到`ArenaAllocator`。 |
| 154 | + |
| 155 | +我们通过暴露`std.mem.Allocator`接口,可以调整如何工作而不破坏`greet`。这不仅简化了资源管理(例如通过`ArenaAllocator`),而且通过重复使用分配来提高了性能(类似于我们做的`retain_with_limit`或`FixedBufferAllocator`的操作)。 |
| 156 | + |
| 157 | +这个示例应该能突出显示我认为明确的分配器提供的两个实际优势: |
| 158 | +1. 简化资源管理(通过类似`ArenaAllocator`的方式) |
| 159 | +2. 通过重用分配来提高性能 |
0 commit comments