内存管理
zig 在内存管理方面采取了类似 C 的方案,完全由程序员管理内存,这也是为什么 zig 没有运行时开销的原因,同时这也是为什么 zig 可以在如此多环境(包括实时软件、操作系统内核、嵌入式设备和低延迟服务器)中无缝工作的原因。
事实上,在 C 开发中最难以调试的 bug 往往是由于错误的内存管理引起的, zig 在此基础上给我们提供了少量的保护,但仅仅是少量的保护,这就要求程序员在需要明白数据在内存中真实存在的模样(这就涉及到计算机组成原理和操作系统的理论知识了,当然还涉及到一点点的汇编知识)。
事实上,zig 本身的标准库为我们提供了多种内存分配模型:
GeneralPurposeAllocator
FixedBufferAllocator
ArenaAllocator
HeapAllocator
c_allocator
page_allocator
StackFallbackAllocator
除了这八种内存分配模型外,还提供了内存池的功能 MemoryPool
你可能对上面的多种内存模型感到很迷惑,C 语言中不就是 malloc
吗,怎么到这里这么多的“模型”,这些模型均有着不同的特点,而且它们之间有一部分还可以叠加使用,zig 在这方面提供了更多的选择,而且不仅仅是这些,你还可以自己尝试实现一个内存模型。
🅿️ 提示
除了这些,还有一些你可能用不到的分配模型:
std.testing.FailingAllocator
std.testing.allocator
std.heap.LoggingAllocator
std.heap.LogToWriterAllocator
std.heap.SbrkAllocator
std.heap.ScopedLoggingAllocator
🅿️ 提示
补充一点,zig 的内存分配并不会自动进行 0 填充,并且 zig 并没有提供 calloc
这种函数,故我们需要手动实现初始化为 0 的操作,不过好在 zig 提供了 std.mem.zeroes
函数,用于直接返回某种类型的 0 值。
GeneralPurposeAllocator
这是一个通用的分配器,当你需要动态内存时,并且还不知道自己应该用什么分配器模型,用这个准没错!
这个分配器的目的不是为了性能,而是为了安全,它支持线程安全,安全检查,检查是否存在泄露等特性,这些特性均可手动配置是否开启。
const std = @import("std");
pub fn main() !void {
// 使用模型,一定要是变量,不能是常量
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
// 拿到一个allocator
const allocator = gpa.allocator();
// defer 用于执行general_purpose_allocator善后工作
defer {
// 尝试进行 deinit 操作
const deinit_status = gpa.deinit();
// 检测是否发生内存泄漏
if (deinit_status == .leak) @panic("TEST FAIL");
}
//申请内存
const bytes = try allocator.alloc(u8, 100);
// 延后释放内存
defer allocator.free(bytes);
}
FixedBufferAllocator
这个分配器是固定大小的内存缓冲区,无法扩容,常常在你需要缓冲某些东西时使用,注意默认情况下它不是线程安全的,但是存在着变体 ThreadSafeAllocator
,使用 ThreadSafeAllocator
包裹一下它即可。
const std = @import("std");
pub fn main() !void {
var buffer: [1000]u8 = undefined;
// 一块内存区域,传入到fixed buffer中
var fba = std.heap.FixedBufferAllocator.init(&buffer);
// 获取内存allocator
const allocator = fba.allocator();
// 申请内存
const memory = try allocator.alloc(u8, 100);
// 释放内存
defer allocator.free(memory);
}
const std = @import("std");
pub fn main() !void {
var buffer: [1000]u8 = undefined;
// 一块内存区域,传入到fixed buffer中
var fba = std.heap.FixedBufferAllocator.init(&buffer);
// 获取内存allocator
const allocator = fba.allocator();
// 使用 ThreadSafeAllocator 包裹, 你需要设置使用的内存分配器,还可以配置使用的mutex
var thread_safe_fba = std.heap.ThreadSafeAllocator{ .child_allocator = allocator };
// 获取线程安全的内存allocator
const thread_safe_allocator = thread_safe_fba.allocator();
// 申请内存
const memory = try thread_safe_allocator.alloc(u8, 100);
// 释放内存
defer thread_safe_allocator.free(memory);
}
ArenaAllocator
这个分配器的特点是你可以多次申请内存,并无需每次用完时进行 free
操作,可以使用 deinit
直接一次回收所有分发出去的内存,如果你的程序是一个命令行程序或者没有什么特别的循环模式,例如web server 或者游戏事件循环之类的,那么推荐你使用这个。
const std = @import("std");
pub fn main() !void {
// 使用模型,一定要是变量,不能是常量
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
// 拿到一个allocator
const allocator = gpa.allocator();
// defer 用于执行general_purpose_allocator善后工作
defer {
const deinit_status = gpa.deinit();
if (deinit_status == .leak) @panic("TEST FAIL");
}
// 对通用内存分配器进行一层包裹
var arena = std.heap.ArenaAllocator.init(allocator);
// defer 最后释放内存
defer arena.deinit();
// 获取分配器
const arena_allocator = arena.allocator();
_ = try arena_allocator.alloc(u8, 1);
_ = try arena_allocator.alloc(u8, 10);
_ = try arena_allocator.alloc(u8, 100);
}
HeapAllocator
这是一个依赖 windows 特性的分配器模型,故仅可在 windows 下可用。
关于这个模型的更多信息,可以参考这里https://learn.microsoft.com/en-us/windows/win32/api/heapapi/
const std = @import("std");
pub fn main() !void {
// 获取分配器模型
var heap = std.heap.HeapAllocator.init();
// 善后工作,但有一点需要注意
// 这个善后工作只有在你手动指定 heapallocaotr 的 heap_handle 字段时,才有效
defer heap.deinit();
// 获取分配器
const allocator = heap.allocator();
// 分配内存
const num = try allocator.alloc(u8, 1);
// free 内存
defer allocator.free(num);
}
c_allocator
这是纯粹的 C 的 malloc
,它会直接尝试调用 C 库的内存分配,使用它需要在 build.zig
中添加上 linkLibC
功能:
const std = @import("std");
pub fn main() !void {
// 用起来和 C 一样纯粹
const c_allocator = std.heap.c_allocator;
const num = try c_allocator.alloc(u8, 1);
defer c_allocator.free(num);
}
🅿️ 提示
它还有一个变体:raw_c_allocator
。
这两者的区别仅仅是 c_allocator
可能会调用 alloc_aligned
而不是 malloc
,会优先使用 malloc_usable_size
来进行一些检查。
而 raw_c_allocator
则是完全只使用 malloc
。
page_allocator
这是最基本的分配器,它仅仅是实现了不同系统的分页申请系统调用。
每次执行分配时,它都会向操作系统申请整个内存页面。单个字节的分配可能会剩下数千的字节无法使用(现代操作系统页大小最小为4K,但有些系统还支持2M和1G的页),由于涉及到系统调用,它的速度很慢,但好处是线程安全并且无锁。
const std = @import("std");
pub fn main() !void {
const allocator = std.heap.page_allocator;
const memory = try allocator.alloc(u8, 100);
defer allocator.free(memory);
}
StackFallbackAllocator
该分配器比较特殊,它会尽量在使用栈上的内存,如果请求的内存量超过了可用的栈空间,那么它将回退到事先制定的分配器,即使用堆内存。
该分配器的目的和内存池类似,都是尽量避免使用堆内存(堆内存相对于栈上分配过慢)。
const std = @import("std");
pub fn main() !void {
// 初始化一个优先使用栈区的分配器
// 栈区大小为256个字节,如果栈区不够用,就会使用page allocator
var stack_alloc = std.heap.stackFallback(
256 * @sizeOf(u8),
std.heap.page_allocator,
);
// 获取分配器
const stack_allocator = stack_alloc.get();
// 申请内存
const memory = try stack_allocator.alloc(u8, 100);
// 释放内存
defer stack_allocator.free(memory);
}
MemoryPool
TIP
内存池,也被称为对象池,是一种内存管理策略。在预先分配的内存块(池)中,当程序需要创建新的对象时,它会从池中取出一个已经分配的内存块,而不是直接从操作系统中申请。同样,当对象不再需要时,它的内存会被返回到池中,而不是被释放回操作系统。
内存池的主要优点是提高了内存分配的效率。通过减少使用系统调用(内存分配需要通过系统调用向 os 申请内存),并且预先分配的内存块大小是固定的,所以分配和回收内存的操作可以在常数时间内完成。
此外,由于内存块在物理上是相邻的,因此内存池还可以减少内存碎片。
内存池也有其缺点,例如,如果内存池的大小设置得不合适(太大或太小),则可能会浪费内存或导致内存不足。
const std = @import("std");
pub fn main() !void {
// 此处为了演示,直接使用page allocator
var pool = std.heap.MemoryPool(u32).init(std.heap.page_allocator);
defer pool.deinit();
// 连续申请三个对象
const p1 = try pool.create();
const p2 = try pool.create();
const p3 = try pool.create();
// 回收p2
pool.destroy(p2);
// 再申请一个新的对象
const p4 = try pool.create();
// 注意,此时p2和p4指向同一块内存
_ = p1;
_ = p3;
_ = p4;
}
除了基本的分配,内存池还支持预分配和指针对齐设置等,源代码可以参考这里memory_pool.zig,它的实现很巧妙,值得一看。
这里有一篇关于最初这个内存池是如何实现的文章:Cool Zig Patterns - Gotta alloc fast
实现内存分配器
待添加,当前你可以通过实现 Allocator
接口来实现自己的分配器。为了做到这一点,必须仔细阅读 std/mem.zig
中的文档注释,然后提供 allocFn
和 resizeFn
。
有许多分配器示例可供查看以获取灵感。查看 std/heap.zig
和 std.heap.GeneralPurposeAllocator