第7章:内存管理

对齐第一版第7章 Memory Management:虚拟地址与页面、stack/heap、placement new和对齐、ownership/RAII、small size optimization及arena/custom allocator。

学习目标

  • 能解释virtual address space、memory pages、page fault与thrashing如何把working set变成性能问题
  • 能区分stack/heap storage、new/delete、placement new、alignment与object lifetime
  • 能设计RAII ownership、small size optimization和arena/custom allocator,并验证时间与内存取舍

机制总览

第7章:内存管理:机制路径

  1. 1

    从“地址、存储、对象、所有权不是一回事”开始

    内存管理至少包含四层:程序使用virtual address;OS把page映射到physical memory或backing store;allocator把较大区域切成storage block;C++在storage中开始和结束object lifetime。最后还要由ownership c…

  2. 2

    virtual address space与memory …

    virtual address space按memory pages管理。reserve一段地址不等于所有physical pages立即驻留;首次触碰可能触发minor page fault,由OS建立映射并提供zero-filled page。若所需数据不在RAM,major fault可能需要…

  3. 3

    stack memory与heap memory

    stack memory通常随function call维护frame,automatic-lifetime local objects常由stack pointer附近storage承载;退出scope时按逆序析构。compiler可依据as-if rule把对象放进register、消除它或采用…

先按顺序建立机制,再进入实验切换阶段并检查失效证据。

章级决策实验

第7章:内存管理:机制与证据

切换《第7章:内存管理》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。

选择推理阶段

当前阶段 · 从“地址、存储、对象、所有权不是一回事”开始

内存管理至少包含四层:程序使用virtual address;OS把page映射到physical memory或backing store;allocator把较大区域切成storage block;C++在storage中开始和结束object lifetime。最后还要由ownership c…

可核验证据

保留可复现基准、输入规模和编译参数,用采样剖析与硬件计数器核对「从“地址、存储、对象、所有权不是一回事”开始」前后的时间和资源变化。

学完《第7章:内存管理》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。

失效—证据矩阵

第7章:内存管理:失效与核验

从“地址、存储、对象、所有权不是一回事”开始

典型失效

若脱离基线与成本模型讨论「从“地址、存储、对象、所有权不是一回事”开始」,局部优化可能只是在移动开销,甚至让缓存、分配或同步瓶颈更严重。

核验证据

保留可复现基准、输入规模和编译参数,用采样剖析与硬件计数器核对「从“地址、存储、对象、所有权不是一回事”开始」前后的时间和资源变化。

virtual address space与memory …

典型失效

若脱离基线与成本模型讨论「virtual address space与memory …」,局部优化可能只是在移动开销,甚至让缓存、分配或同步瓶颈更严重。

核验证据

保留可复现基准、输入规模和编译参数,用采样剖析与硬件计数器核对「virtual address space与memory …」前后的时间和资源变化。

stack memory与heap memory

典型失效

若脱离基线与成本模型讨论「stack memory与heap memory」,局部优化可能只是在移动开销,甚至让缓存、分配或同步瓶颈更严重。

核验证据

保留可复现基准、输入规模和编译参数,用采样剖析与硬件计数器核对「stack memory与heap memory」前后的时间和资源变化。

每个判断都必须能落到观测、测试或产物,不能只凭代码表面推测。

从“地址、存储、对象、所有权不是一回事”开始

内存管理至少包含四层:程序使用virtual address;OS把page映射到physical memory或backing store;allocator把较大区域切成storage block;C++在storage中开始和结束object lifetime。最后还要由ownership contract决定谁负责销毁对象、何时释放storage。把这些层混成“new很慢”会隐藏真正瓶颈。

先预测热点来自allocation frequency、working-set pressure、object lifetime、fragmentation、zeroing、page fault还是cache locality,再选择实验。替换allocator只能改变其中一部分;若问题是遍历分散对象或数据规模超出memory,换一个更快的free list不会自动修复。

virtual address space与memory pages

virtual address space按memory pages管理。reserve一段地址不等于所有physical pages立即驻留;首次触碰可能触发minor page fault,由OS建立映射并提供zero-filled page。若所需数据不在RAM,major fault可能需要I/O。TLB缓存最近的address translation,working set跨越大量pages会增加translation miss。

resident set是当前驻留physical memory的页面集合,working set是某一时间窗口真正活跃的页面。访问模式若超过可用memory并不断把即将再次访问的page换出,就会发生thrashing:程序大部分时间处理page movement而不是业务计算。顺序扫描大文件、random pointer graph与多进程竞争会产生不同fault和cache行为,必须分别测量。

large/huge pages可扩大TLB覆盖,但会改变allocation granularity、fragmentation、fault成本和部署权限;它们不是默认优化。memory mapping也不保证数据“免费加载”:mmap只建立地址关系,实际I/O仍可能在fault路径发生。测量时记录RSS、faults、I/O、TLB events和访问顺序。

stack memory与heap memory

stack memory通常随function call维护frame,automatic-lifetime local objects常由stack pointer附近storage承载;退出scope时按逆序析构。compiler可依据as-if rule把对象放进register、消除它或采用其他storage,所以“局部变量必在栈上”不是语言保证。stack容量有限,深递归或大型local array可能overflow。

heap memory表示由dynamic allocation管理、lifetime不受当前scope结束直接约束的storage。allocator可能从thread cache、size class、free list或OS mapping满足请求;small allocation不必每次system call,成本也不能写死为固定纳秒。contention、fragmentation、metadata、page touch与locality共同决定表现。

选择不是“能上栈绝不上堆”。大对象会占用有限stack,跨scope/shared ownership需要独立lifetime,返回值优化也不等价于“对象必须在stack”。真正策略是减少不必要allocation、让owner明确、批量/复用适合的lifetime,并以目标allocator和workload测量。

storage allocation与object lifetime

new expression执行两件事:调用allocation function(通常operator new)取得适当size/alignment的raw storage,再在其中construct object;delete expression先调用destructor,再把storage交给deallocation function(通常operator delete)。new/delete operators可以定制storage策略,但必须匹配size、alignment、异常与对应释放路径。

void* storage = ::operator new(sizeof(Session),
                               std::align_val_t{alignof(Session)});
Session* session = nullptr;
 
try {
    session = ::new (storage) Session(config); // placement new
    session->run();
    session->~Session();
    ::operator delete(storage, std::align_val_t{alignof(Session)});
} catch (...) {
    if (session == nullptr) {
        ::operator delete(storage, std::align_val_t{alignof(Session)});
    }
    throw;
}

placement new不分配storage,只在caller提供的地址开始object lifetime。成功构造后必须在正确时机显式析构,再由原storage provider回收;constructor抛出时对象lifetime未开始,cleanup路径不同。现实代码应把这些步骤封装进RAII pool/allocator,而不是散落手写。

memory alignment与padding

每个type有alignment requirement,object address必须满足它。struct layout会在members之间或末尾插入padding,以保证每个member和array下一元素对齐;member顺序因此影响sizeof与每条cache line可容纳元素数。over-aligned type的dynamic storage必须走支持对应alignment的allocation/deallocation形式。

struct alignas(64) WorkerCounter {
    std::atomic<std::uint64_t> completed{0};
};
 
static_assert(alignof(WorkerCounter) >= 64);
static_assert(sizeof(WorkerCounter) % alignof(WorkerCounter) == 0);

更大alignment不必然更快。alignas(64)可隔离频繁写入的per-thread counters,降低false sharing,却让每个counter至少占一line;如果对象只读或数量巨大,footprint可能更差。SIMD instruction是否要求/受益于alignment取决于ISA与generated code。用alignofsizeof、layout dump和hardware counters验证,不用固定cache-line假设替代目标机器。

memory ownership与RAII

memory ownership回答谁负责结束object lifetime并释放storage。RAII把resource acquisition绑定constructor,把release绑定destructor,使normal return与exception unwinding共享cleanup路径。value member和container优先;需要dynamic unique owner时用std::unique_ptr,共享lifetime才用std::shared_ptr,非拥有观察者必须显式保证owner活着。

auto session = std::make_unique<Session>(config);
std::vector<std::unique_ptr<Node>> nodes;
nodes.emplace_back(std::make_unique<Node>());
 
std::shared_ptr<const Catalog> shared = loadCatalog();
std::weak_ptr<const Catalog> observer = shared;

shared_ptr包含shared ownership control block,copy会更新reference count;原子更新、control-block access和可能的extra allocation都有成本。make_shared通常把object与control block组合分配,但weak references可能延长整块storage占用。cycle不会自动打破,需要用weak_ptr表达非拥有edge。性能优化不能以raw pointer替换owner却丢失lifetime contract。

small size optimization避免常见小分配

small size optimization在object内部保留小buffer:value较小时直接存于object,超过capacity才转到external allocation。string的small-string optimization与function-like wrapper的small-buffer optimization都属于此类。threshold、layout和eligible type是implementation detail,不能依赖固定字节数或假设所有小值都不分配。

优化交换了每个object更大的固定size、branch/state与move规则,以减少常见allocation。大量empty/small objects可能因inline buffer扩大working set;large values仍会allocate。应测value-size distribution、object count、copy/move和allocation,而不是只测一个短string。

arena与custom memory allocator

arena从较大block顺序切出storage,单次allocate常近似pointer bump;多个对象共享arena lifetime,reset时批量回收。它适合request、frame、parser AST等“集中创建、一起销毁”的阶段性对象。若对象持有外部resource或non-trivial destructor,arena仍需记录并执行destruction,不能只丢掉bytes。

custom memory allocator还包括fixed-size pool、free list、slab、stack allocator和标准allocator/PMR接口。pool适合同size反复reuse,arena适合同lifetime批量release;general-purpose allocator需要处理多size、多thread与任意free order。选择器必须匹配allocation pattern,不能只比较单线程allocate microbenchmark。

fragmentation分internal与external:size class/block内部浪费是internal,空闲空间被切散无法满足大请求是external。arena几乎没有individual free,却可能因phase过长保留peak memory;thread-local pool减少contention但增加每thread reserve和跨thread free复杂度。报告allocation latency还要同时报告peak/resident bytes、waste、reset/destructor cost与failure behavior。

第7章实验协议

  1. 分别测reserve-only、first-touch cold pages与warm traversal,记录faults、RSS和time。
  2. 扩大working set并改变访问顺序,观察cache/TLB miss到thrashing的转折。
  3. 分离operator new storage获取、placement construction、destruction与deallocation成本。
  4. 对不同member order输出sizeof/alignof/offset,测array scan与false sharing workload。
  5. 比较value、unique_ptr、shared_ptr/make_shared的allocation、copy与lifetime行为。
  6. 统计string/callable size分布和实际allocation,验证small size optimization交叉点。
  7. 对request-lifetime objects比较general allocator、arena和pool的time、peak RSS与waste。
  8. 为arena加入throwing constructor、non-trivial destructor、reset和跨thread misuse tests。

小结

  • virtual address space通过memory pages映射physical memory;fault、TLB和working set决定访问成本
  • thrashing是页面频繁换入换出,替换小对象allocator无法解决超大working set
  • stack与heap代表不同storage/lifetime策略,语言不保证每个local对象的物理位置
  • new expression组合allocation与construction;placement new只开始object lifetime
  • alignment与padding影响合法地址、object size、cache密度和false sharing
  • RAII把ownership与cleanup绑定;unique/shared/weak分别表达不同lifetime关系
  • small size optimization用更大inline representation交换常见小值的allocation
  • arena与custom allocator必须匹配size、thread、free order和共同lifetime

资料与写作方式声明

本章以C++ High Performance, First Edition, Chapter 7: Memory Management权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

名词解释

本章出现的专业名词,用大白话再讲一遍。

virtual address space

由页表映射到物理或后备存储的进程逻辑地址集合。

memory page

虚拟内存映射、保护与fault处理的固定粒度块。

thrashing

working set失配导致页面频繁换入换出的状态。

stack memory

与调用和scope紧密关联的自动存储区域。

heap memory

由动态分配器管理并通过owner控制lifetime的storage。

placement new

在调用方提供的raw storage中开始object lifetime。

padding

为alignment和array stride插入的非业务字节。

RAII

把resource acquisition/release绑定构造析构的技术。

small size optimization

以object内inline buffer避免常见小值allocation的策略。

arena

按共同lifetime批量分配和回收对象storage的区域。

练习

  1. 问题 1:程序reserve了8 GiB地址但RSS很小,第一次扫描却很慢,怎样解释并验证? 区分地址、commit与resident page。
  1. 问题 2:实现arena中的placement construction时,constructor可能抛异常且对象有destructor。 设计正确cleanup。
  1. 问题 3:大量短字符串是否一定应依赖small size optimization而不用arena? 设计对比实验。

讨论

评论区加载中…