首页
/ mimalloc Windows 原语与 ETW 分配跟踪:codebase-memory-mcp 内存层的系统级剖析

mimalloc Windows 原语与 ETW 分配跟踪:codebase-memory-mcp 内存层的系统级剖析

2026-09-05 09:03:19作者:翟萌耘Ralph

codebase-memory-mcp 的生产构建直接以 mimalloc 作为进程级内存分配器(见 src/foundation/mem.c),而 mimalloc 在 Windows 上的行为由 vendored 源码目录 vendored/mimalloc/src/prim/windows 中的"原语层"(prim)决定。本文以 vendored/mimalloc/src/prim/windows/readme.md 为纲,讲清两件事:该目录中 prim.c 如何封装 Windows OS 分配原语(VirtualAlloc 体系、大页/巨页、NUMA 感知、进程/线程生命周期钩子),以及 etw.manetw.h 的 Event Tracing for Windows(ETW)管线如何让你用 wpr 抓取 mimalloc 的逐次分配/释放事件并在 Windows Performance Analyzer(WPA)中分析。

一、目录定位:readme 声明的两个组件

readme 原文对目录职责的划分只有两条,但可以精确映射到实际文件:

  1. prim.c 包含 Windows 平台的 OS 分配原语——即 mimalloc 抽象层(_mi_prim_* 接口)在 Windows 上的全部实现;
  2. etw.hetw.man 生成,manifest 中定义了 mimalloc 的两个事件:100 表示分配(allocation),101 表示释放(free);配套的 etw-mimalloc.wprp 是 Windows Performance Recorder 的采集 profile。

对应文件清单:prim.cetw.manetw.hetw-mimalloc.wprp

需要注意的前提:prim.c 文件头注释写明 "This file is included in src/prim/prim.c"(见 prim.c),即它不是独立编译单元,而是由 mimalloc 的平台分发文件在 Windows 构建时 include 进来,通过条件编译参与链接。

二、OS 分配原语:prim.c 的核心机制

2.1 初始化:动态绑定 Windows API 与系统参数

入口是 _mi_prim_mem_initprim.c#L144-L214),它完成三组工作:

  • 读取系统内存参数GetSystemInfo 取页大小(dwPageSize)与分配粒度(dwAllocationGranularity,代码中默认按 64 KiB 处理,见 prim.c#L142),并从 lpMaximumApplicationAddress 推算用户态虚拟地址位数;
  • 动态绑定新式 APIVirtualAlloc2 仅在 Windows 10 / Server 2016 起提供,为了兼容老系统,prim.c 通过 LoadLibrary/GetProcAddress 动态查找,并优先取 VirtualAlloc2FromApp(Windows Store 应用可用,见 prim.c#L164-L177)。同理还绑定了 NtAllocateVirtualMemoryEx(1 GiB 巨页)、Windows 7+ 的 NUMA 扩展 API 与 GetLargePageMinimum
  • 物理内存探测:动态调用 GetPhysicallyInstalledSystemMemory 填充 config->physical_memory_in_kib,并读取 GetVersionExW 缓存主/次版本号,供后续线程池判断使用。

代码中还自行定义了 MI_MEM_EXTENDED_PARAMETER / MI_MEM_ADDRESS_REQUIREMENTS 等最小结构(prim.c#L36-L55),注释说明这是为了"能在旧版 SDK 上编译"——这是理解该文件大量手工函数指针声明的原因。

2.2 分配路径:VirtualAlloc 三级回退与 OOM 重试

_mi_prim_allocprim.c#L349-L358)按 commit 标志组合 MEM_RESERVE/MEM_COMMIT 后进入 win_virtual_alloc,后者是三级策略:

  1. 大页(2 MiB)优先:当 mi_option_allow_large_os_pages 开启且是"reserve+commit"分配时,带 MEM_LARGE_PAGES 重试。源码里有个值得注意的熔断器 large_page_try_okprim.c#L317-L338):一旦大页分配失败,计数器置 10,此后 10 次分配不再尝试大页——注释解释原因是"大页失败后 VirtualAlloc 调用会变得非常昂贵";
  2. 对齐分配win_virtual_alloc_prim_onceprim.c#L246-L273)在 64 位下先尝试 2 TiB 之上的虚拟地址区间提示(配合 mimalloc 自身的对齐 hint),对齐要求超过分配粒度时改用 VirtualAlloc2 + MiMemExtendedParameterAddressRequirements
  3. 最后回退到普通 VirtualAlloc

外层 win_virtual_alloc_primprim.c#L287-L313)实现了 mi_option_retry_on_oom 选项:对 OOM 类错误码(ERROR_COMMITMENT_MINIMUMERROR_COMMITMENT_LIMITERROR_PAGEFILE_QUOTAERROR_NOT_ENOUGH_MEMORY)在最多 10 次、总时长约 2.2 秒内做递增等待重试,这是针对 issue #894 的"先坚持一下,内存也许就腾出来了"策略。

大页的使能本身也有 Windows 特有门槛:win_enable_large_os_pages_onceprim.c#L95-L128)必须成功 OpenProcessToken + AdjustTokenPrivileges 激活 SeLockMemoryPrivilege(组策略中的"锁定内存页"权限),失败时只打印警告并退回普通页。

2.3 提交/取消提交/重置与释放

  • _mi_prim_commitVirtualAlloc(addr, size, MEM_COMMIT, PAGE_READWRITE)prim.c#L368-L383);
  • _mi_prim_decommitVirtualFree(..., MEM_DECOMMIT),且保守地总是置 *needs_recommit = trueprim.c#L385-L389);
  • _mi_prim_resetMEM_RESETprim.c#L391-L400),对应 mimalloc 的"快速回收页"语义;
  • _mi_prim_freeprim.c#L221-L239)处理了一个 Windows 特有的边角:若 VirtualFreeERROR_INVALID_ADDRESS(回退路径返回了对齐后的内部指针而非区域基址),先用 VirtualQueryAllocationBase,只要偏移小于 4 MiB 就用基址再释放一次。

2.4 巨页(1 GiB)与 NUMA

_mi_prim_alloc_huge_os_pagesxprim.c#L418-L457)体现了完整的回退链:先尝试 NtAllocateVirtualMemoryEx + MI_MEM_EXTENDED_PARAMETER_NONPAGED_HUGE 属性申请 1 GiB 大页(可叠加 MiMemExtendedParameterNumaNode 指定 NUMA 节点),失败则置原子标志 mi_huge_pages_available = 0 永久放弃巨页,降级为 2 MiB 大页;再不行则用 VirtualAlloc2 做 NUMA 感知分配,最旧系统退回普通 VirtualAlloc

NUMA 拓扑查询(_mi_prim_numa_node / _mi_prim_numa_node_countprim.c#L470-L517)同样双轨制:优先 Win7+ 扩展 API(支持 >64 核,修复 issue #277),否则回退到受限的旧 API;节点计数时还会逐个向下检查"最高号节点是否真的挂了处理器"(issue #282)。

2.5 进程/线程生命周期:TLS + CRT 组合钩子

Windows 上自动初始化 mimalloc 的时机问题由 mi_win_main 与一组可切换策略解决(prim.c#L719-L745)。默认策略 MI_WIN_INIT_USE_CRT_TLS 是"组合拳":

  • 通过 PE 的 TLS 回调段(.CRT$XLB/.CRT$XLY 中的 _mi_tls_callback_pre/post,32/64 位分别用 data_seg/const_seg 宏,见 prim.c#L811-L854)在构造函数之前 attach、析构之后 detach;
  • 对 DLL 场景额外 atexit(&mi_crt_done),保证 DLL 中 CRT 结束发生在 TLS detach 之后(见 mi_crt_init 注释,prim.c#L775-L782);
  • 另有 MI_WIN_INIT_USE_RAW_DLLMAIN(通过 _pRawDllMain 钩子,注释指出会与 Boost 等同样钩子此指针的库冲突)以及更旧的 DllMain/TLS/FLS 选项。

mi_win_main 本身按事件分发:DLL_PROCESS_ATTACH 触发 _mi_auto_process_init()DLL_PROCESS_DETACH 触发 _mi_auto_process_done()DLL_THREAD_DETACH(未重定向时)触发 _mi_thread_done(NULL)prim.c#L719-L731)。

2.6 其他平台原语速览

同一文件还提供:基于 QueryPerformanceCounter 的毫秒时钟(prim.c#L524-L539);进程内存信息 _mi_prim_process_info 动态加载 psapi.dll 取工作集/提交量/缺页计数(prim.c#L559-L588)——主项目 src/foundation/diagnostics.c 正是围绕 mi_process_info 的字段做跨平台校正的;线程池检测通过读 TEB 偏移 0x1778(64 位)判断 pool_dataprim.c#L693-L704);随机数优先 bcrypt.dllBCryptGenRandom,可用 MI_USE_RTLGENRANDOM 切换到 RtlGenRandom(注释记录了 C++ 下抛异常与 VS 调试器死锁的历史问题);stderr 输出则在控制台/重定向两种情况下分别走 WriteConsoleA/WriteFile,避免 C 运行时在主线程退出后无法处理 locale 相关输出(prim.c#L594-L627)。

三、ETW 管线:从 manifest 到 WPA

3.1 事件定义与生成物

etw.man 是 ETW provider 的 XML manifest。文件首行注释给出了生成命令(在 Visual Studio 命令提示符下运行 mc .\etw.man,见 etw.man),生成的 etw.h 是典型的 Message Compiler 输出。从生成物可以读出全部事实:

  • Provider 名为 microsoft-windows-mimalloc,GUID 为 138f4dbb-ee04-4899-aa0a-572ad4475779etw.h#L666);
  • 两个事件描述符:ETW_MI_ALLOC = 0x64(即十进制 100)、ETW_MI_FREE = 0x65(即十进制 101),与 readme 的"100 is an allocation, 101 is for a free"逐字吻合(etw.h#L675-L678);
  • 两个事件共用同一个双 unsigned __int64 模板(地址 + 大小),写入宏为 EventWriteETW_MI_ALLOC(Address, Size) / EventWriteETW_MI_FREE(Address, Size)etw.h#L824-L859)。

这些宏如何接到 mimalloc 分配路径上,由 include/mimalloc/track.h 完成映射:

#define mi_track_init()                           EventRegistermicrosoft_windows_mimalloc()
#define mi_track_done()                           EventUnregistermicrosoft_windows_mimalloc()
#define mi_track_malloc_size(p,reqsize,size,zero) EventWriteETW_MI_ALLOC((UINT64)(p), size)
#define mi_track_free_size(p,size)                EventWriteETW_MI_FREE((UINT64)(p), size)

调用点在分配快路径上,例如 src/alloc.c#L150src/alloc-aligned.c#L223mi_track_malloc。从源码结构看,该管线以 MI_TRACK_* 编译开关启用,且由于 ETW 写入宏在事件未启用时只是位测试后直接短路(MCGEN_EVENT_ENABLED 检查,见 etw.h#L236-L238),非跟踪构建下的开销被设计为接近零。

3.2 采集 profile:etw-mimalloc.wprp 的内容

readme 说该 profile 面向 WPR,实际文件是一个 XML profile,核心配置为:

  • Mimalloc_Collector 事件采集器:BufferSize=1024Buffers=100
  • EventProvider Name="138f4dbb-ee04-4899-aa0a-572ad4475779"NonPagedMemory="true" Stack="true"——即开启调用栈采集、使用非分页内存缓冲,适合生产级抓取;
  • EventFilters FilterIn="true" 只放行 EventId 100EventId 101,精确对应分配/释放两个事件;
  • 系统采集器附带 Loader 关键字,Profile 命名 CustomHeap.Verbose.File,日志模式为 File,便于事后导入 WPA。

3.3 实操:wpr 抓取与 WPA 分析

按 readme 给出的流程(需管理员权限的提示符):

> wpr -start src\prim\windows\etw-mimalloc.wprp -filemode
> <my mimalloc program>
> wpr -stop test.etl

然后打开 test.etl 到 Windows Performance Analyzer 中分析。对应到本仓库的上下文:如果你的 mimalloc 程序是链接了 vendored mimalloc 的可执行文件(或生产构建的 codebase-memory-mcp 相关 Windows 程序,其分配器行为即由 prim.c 决定),test.etl 中将出现每个 EventId=100/101 的分配/释放事件,携带地址、大小与调用栈,可直接在 WPA 中做堆行为的时间线分析。注意 readme 中路径是相对于 mimalloc 源码树写作的 src\prim\windows\...,在本仓库中实际文件位置为 etw-mimalloc.wprp,使用时请替换为实际路径。

四、小结:这份 prim 层对使用方意味着什么

  • 分配语义:mimalloc 在 Windows 上是 reserve/commit 两段的 VirtualAlloc 模型,MEM_RESET 快速回收、MEM_DECOMMIT 归还物理页,大页/巨页按"能力探测 + 熔断"策略渐进启用;
  • 兼容策略:所有 Win10+ 新 API 均运行时动态绑定,老系统自动降级——这正是它能在 CI 矩阵覆盖旧 Windows 的原因;
  • 可观测性:ETW provider 是 mimalloc 的"官方"分配级观测通道,100/101 两个事件加 WPR profile 构成完整的抓取-分析闭环,且事件未启用时对热路径几乎零成本;
  • 对本项目的意义:codebase-memory-mcp 通过 src/foundation/mem.c 统一管理 mimalloc 的配置(提交策略、page reclaim 等),而 Windows 构建下这些策略最终都落到本文分析的 prim.c 原语实现上。

若要进一步验证,可从三个入口切入:跟踪宏映射 track.h、ETW 生成头 etw.hEventWriteETW_MI_ALLOC 的定义,以及 src/foundation/mem.c 中主项目对 mimalloc 选项的调优注释。

登录后查看全文
热门项目推荐
相关项目推荐