[{"data":1,"prerenderedAt":1229},["ShallowReactive",2],{"\u002F2026\u002Fnginx_c++":3,"surround-\u002F2026\u002Fnginx_c++":1218},{"id":4,"title":5,"body":6,"categories":1148,"date":1150,"description":1151,"draft":1152,"extension":1119,"image":1153,"meta":1154,"navigation":1204,"path":1205,"permalink":1153,"published":1153,"readingTime":1206,"recommend":1153,"references":1153,"seo":1211,"sitemap":1212,"stem":1213,"tags":1214,"type":1216,"updated":1150,"__hash__":1217},"content\u002Fposts\u002F2026\u002F内存池仿Nginx_C++实现.md","内存池仿Nginx_C++实现",{"type":7,"value":8,"toc":1123},"minimark",[9,13,25,28,36,62,65,68,79,86,90,100,103,106,109,143,149,157,171,173,177,180,192,203,210,223,228,241,248,261,269,278,296,302,329,335,354,364,366,371,386,392,400,414,437,443,450,520,526,528,532,538,542,545,552,560,563,639,713,751,780,782,785,819,827,835,858,861,890,897,904,917,948,961,963,966,969,977,980,1005,1010,1033,1037,1039,1043,1059,1114],[10,11,12],"h2",{"id":12},"引言",[14,15,18,22],"poetry",{"author":16,"title":17},"Maksim","写在前面",[19,20,21],"p",{},"Nginx 内存池，\n我读了不止一次源码——\n每次都觉得设计很妙。",[19,23,24],{},"最终，\n我用 C++ 把它写了一遍，\n然后接进了我的项目里。",[19,26,27],{},"本篇不是逐行剖析 Nginx 源码的学习笔记——网上这类文章已经很多。这里记录的是我读懂源码之后，对应的 C++ 实现思路。",[19,29,30,31,35],{},"实现之后，我把它接入了 C++17 的 ",[32,33,34],"code",{"code":34},"std::pmr::memory_resource","，作为底层内存分配源，用来优化项目里的 HTTP 路由解析。（这部分本篇不讲。）",[37,38,41],"alert",{"title":39,"type":40},"本文目标","info",[19,42,43,44,48,49,52,53,58,59],{},"不逐行解析 Nginx 内存池源码，但",[45,46,47],"strong",{},"设计思想","、以及 ",[45,50,51],{},"C++ 在 Coding 层面与 C 的差异","，会在行文中对照说明。 ",[54,55,57],"badge",{":round":56},"true","C++"," ",[54,60,61],{":round":56},"Nginx",[63,64],"hr",{},[10,66,67],{"id":67},"核心内存管理概念",[19,69,70,71,74,75,78],{},"概念部分稍显枯燥，但要理解 Nginx 内存池的设计，这是绕不开的前置。核心是两个：",[45,72,73],{},"Bump allocation"," 与 ",[45,76,77],{},"Arena allocator","。",[19,80,81,82,85],{},"它们解决的是同一类问题——小对象频繁分配带来的开销。前提是这批对象",[45,83,84],{},"生命周期一致","，可以整体一次性释放。",[87,88,77],"h3",{"id":89},"arena-allocator",[19,91,92,93,99],{},"传统分配（如 ",[32,94,97],{"className":95,"code":97,"language":98},[96],"language-cpp","malloc","cpp","）每次都向操作系统要内存，频繁申请会带来内存碎片和性能损耗。",[19,101,102],{},"Arena 分配则相反：提前向系统要一块足够大的内存（即 Arena），之后所有小对象都在这块\"场地\"内部划分，不再陷入内核。",[87,104,73],{"id":105},"bump-allocation",[19,107,108],{},"Bump allocation 译作\"指针碰撞\"，是一种靠指针移动来分配内存的技术，通常配合 Arena 使用。",[110,111,112],"card-list",{},[113,114,115,122,128,134],"ul",{},[116,117,118,121],"li",{},[32,119,120],{"code":120},"start","：记录起始位置",[116,123,124,127],{},[32,125,126],{"code":126},"end","：记录结束位置",[116,129,130,133],{},[32,131,132],{"code":132},"last","：指向当前尚未分配的位置",[116,135,136,139,140,142],{},[45,137,138],{},"分配"," = 把 ",[32,141,132],{"code":132}," 向后推一段，划出请求的大小并返回",[19,144,145,146,148],{},"需要分配时，不做任何复杂查找，直接把 ",[32,147,132],{"code":132}," 指针向后**\"推\"（Bump）**一段，划出请求的大小并返回。",[19,150,151,152,156],{},"这种方式很适合堆上频繁产生的临时对象。比如 C++ 里的 ",[32,153,155],{"className":154,"code":155,"language":98},[96],"std::string","，小字符串的频繁分配与释放可能会占据一部分 CPU 热点。",[37,158,161],{"title":159,"type":160},"代价","warning",[19,162,163,166,167,170],{},[45,164,165],{},"无法单独释放某个对象","。回收只能整体进行——析构内存后令 ",[32,168,169],{"code":169},"last = start","，达到重置效果。指针只能前向移动或整体重置，没有中间状态。",[63,172],{},[10,174,176],{"id":175},"nginx-内存池结构","Nginx 内存池结构",[19,178,179],{},"动手写 C++ 版本之前，得先看懂 Nginx 自身的结构设计。它的内存池结构体内部嵌套了多条链表。",[181,182,189],"pre",{"className":183,"code":185,"filename":186,"language":187,"meta":188},[184],"language-c","\u002F\u002F typedef struct ngx_pool_s ngx_pool_t in \u003Cngx_core.h>\n\nstruct ngx_pool_s {\n  ngx_pool_data_t d;\n  size_t max; \u002F\u002F 4095\n  ngx_pool_t *current;\n  ngx_chain_t *chain;\n  ngx_pool_large_t *large;\n  ngx_pool_cleanup_t *cleanup;\n  ngx_log_t *log;\n};\n","ngx_palloc.h","c","icon=tabler:brand-nginx",[32,190,185],{"__ignoreMap":191},"",[19,193,194,195,198,199,202],{},"实际编写时，我删掉了 ",[32,196,197],{"code":197},"chain"," 和 ",[32,200,201],{"code":201},"log"," 两个字段：前者服务于 I\u002FO buffer，后者是调试日志，都与内存池的核心机制无关。",[181,204,208],{"className":205,"code":206,"filename":207,"language":187,"meta":191},[184],"struct ngx_pool_s {\n  ngx_pool_data_t d;\n  size_t max; \u002F\u002F 4095\n  ngx_pool_t *current;\n  ngx_pool_large_t *large;\n  ngx_pool_cleanup_t *cleanup;\n};\n","ngx_palloc_trimmed.h",[32,209,206],{"__ignoreMap":191},[19,211,212,198,215,218,219,222],{},[32,213,214],{"code":214},"large",[32,216,217],{"code":217},"cleanup"," 留到后文，这里先把它们都当作普通链表看待，从 ",[32,220,221],{"code":221},"ngx_pool_data_t d"," 入手。",[87,224,226],{"id":225},"ngx_pool_data_t",[32,227,225],{"code":225},[19,229,230,231,234,235,237,238,240],{},"Nginx 小内存的基本单位，我称之为 ",[32,232,233],{"code":233},"chunk","，而 ",[32,236,225],{"code":225}," 正是用来描述一个 ",[32,239,233],{"code":233}," 的。",[181,242,246],{"className":243,"code":244,"filename":245,"language":187,"meta":191},[184],"typedef struct {\n  u_char *last;\n  u_char *end;\n  ngx_pool_t *next;\n  ngx_uint_t failed;\n} ngx_pool_data_t;\n","ngx_pool_data_t.h",[32,247,244],{"__ignoreMap":191},[19,249,250,251,253,254,257,258,260],{},"举个例子。假设堆上分配了一个 ",[32,252,233],{"code":233},"，大小为 ",[32,255,256],{"code":256},"chunk_size","，由一个 ",[32,259,221],{"code":221}," 来描述：",[181,262,267],{"className":263,"code":265,"language":266,"meta":191},[264],"language-text","     ----------------------\n     |已分配   | 未用空间    |\n     ----------------------\n              ^           ^\nd->last = ----|           |\n                          |\nd->end  = ----------------|\nd->next = 指向下一个chunk\nd.failed = 失败次数(详见下文代码)\n","text",[32,268,265],{"__ignoreMap":191},[19,270,271,272,274,275,277],{},"这就是 Arena 与 Bump 的图解：通过 bump ",[32,273,132],{"code":132}," 指针快速为小对象划出地址，",[32,276,126],{"code":126}," 标记边界、用于越界检查。",[37,279,282],{"title":280,"type":281},"这里有两个值得澄清的点","question",[113,283,284,289],{},[116,285,286,288],{},[32,287,221],{"code":221}," 在栈上还是堆上？需要用户手动申请吗？",[116,290,291,74,293,295],{},[32,292,221],{"code":221},[32,294,233],{"code":233}," 是分开存储的吗？",[19,297,298,301],{},[45,299,300],{},"第一个问题："," 不需要用户申请，交由内存池自己处理，放在堆上。",[19,303,304,307,308,311,312,314,315,317,318,321,322,324,325,328],{},[45,305,306],{},"第二个问题："," 这是 Nginx 设计的一个理解关键——结构体本身",[45,309,310],{},"内嵌","进 ",[32,313,233],{"code":233},"。原本的 ",[32,316,256],{"code":256},"，会在最前面取出 ",[32,319,320],{"code":320},"sizeof(ngx_pool_data_t)"," 的空间存放它自己。也就是说，前 ",[32,323,320],{"code":320}," 字节是描述信息本身，后面 ",[32,326,327],{"code":327},"chunk_size - sizeof(ngx_pool_data_t)"," 才是真正能分给对象的可用空间（暂不考虑内存对齐）。",[181,330,333],{"className":331,"code":332,"language":266,"meta":191},[264],"       -------------------------|\n       |                        ^\n     ----------------------------------------\n     |last|end|next|failed|已分配| 未用空间    |\n     ----------------------------------------\n            |                                ^\n            ---------------------------------|\n",[32,334,332],{"__ignoreMap":191},[37,336,338],{"title":337},"内嵌式结构的取舍",[19,339,340,343,344,347,348,350,351,353],{},[45,341,342],{},"好处","：省了一次 ",[32,345,97],{"className":346,"code":97,"language":187},[184],"。\n",[45,349,159],{},"：占用了一部分 ",[32,352,233],{"code":233}," 的可用内存。",[19,355,356,357,360,361,363],{},"以上讨论适用于",[45,358,359],{},"第二块及之后","的 ",[32,362,233],{"code":233},"。首块是特殊的，下一节单独说明。",[63,365],{},[87,367,369],{"id":368},"ngx_pool_t",[32,370,368],{"code":368},[19,372,373,374,376,377,379,380,382,383,385],{},"前面的结构代码已经表明，",[32,375,368],{"code":368}," 本身就包含一个 ",[32,378,225],{"code":225},"。沿用 ",[32,381,225],{"code":225}," 的内嵌思路，Nginx 把首块 ",[32,384,233],{"code":233}," 直接嵌进了内存池本体。",[181,387,390],{"className":388,"code":389,"language":266,"meta":191},[264],"\u002F\u002F first chunk\n      -----------------------|\n      |                     d.last\n     -------------------------------------\n     |d|max|current|...|已分配| 未用空间    |\n     -------------------------------------\n            |                            d.end\n            ------------------------------|\n",[32,391,389],{"__ignoreMap":191},[19,393,394,395,397,398,78],{},"所以首块与其余 chunk 的区别就在于内嵌内容不同：首块内嵌的是内存池本体 ",[32,396,368],{"code":368},"，第二块及之后内嵌的则是更轻量的 ",[32,399,225],{"code":225},[19,401,402,403,410,411,413],{},"更准确地说，首块和其余每个 chunk（连同各自内嵌的结构体）的",[45,404,405,406,409],{},"整体 ",[32,407,408],{"code":408},"size"," 是相同的","，但真正可用于分配的 ",[32,412,256],{"code":256}," 不同，因为内嵌结构体的开销不一样：",[110,415,416],{},[113,417,418,428],{},[116,419,420,423,424],{},[45,421,422],{},"首块","：",[32,425,427],{"className":426,"code":427,"language":98},[96],"chunk_size = size - sizeof(ngx_pool_t);",[116,429,430,423,433],{},[45,431,432],{},"其余",[32,434,436],{"className":435,"code":436,"language":98},[96],"chunk_size = size - sizeof(ngx_pool_data_t);",[19,438,439,440,442],{},"理清了内嵌关系，再回头看 ",[32,441,368],{"code":368}," 的各个字段：",[181,444,448],{"className":445,"code":446,"filename":447,"language":187,"meta":191},[184],"typedef struct ngx_pool_s {\n  ngx_pool_data_t d;\n  size_t max; \u002F\u002F 4095\n  ngx_pool_t *current;\n  ngx_pool_large_t *large;\n  ngx_pool_cleanup_t *cleanup;\n} ngx_pool_t;\n","ngx_pool_t.h",[32,449,446],{"__ignoreMap":191},[451,452,454,469,487,508],"folding",{":open":56,"title":453},"📑 字段逐一解释",[19,455,456,461,462,464,465,468],{},[45,457,458],{},[32,459,460],{"code":460},"d"," 是内存池本体内嵌的那个 ",[32,463,225],{"code":225},"，它内部的 ",[32,466,467],{"code":467},"next"," 指针把后续所有 chunk 串成一条单链表——这是整个池子的骨架。",[19,470,471,476,477,479,480,483,484,486],{},[45,472,473],{},[32,474,475],{"code":475},"max"," 表示每个 chunk（含内嵌结构体）的 ",[32,478,408],{"code":408},"，",[32,481,482],{"code":482},"4095"," 是单个 chunk 的上限。这个值同时也是小内存与大内存分配的分水岭：超过 ",[32,485,475],{"code":475}," 的请求会走单独的大内存路径。",[19,488,489,494,495,497,498,501,502,504,505,507],{},[45,490,491],{},[32,492,493],{"code":493},"current"," 指向当前\"有效\"的 chunk——所谓有效，是指它内部还有足够空间分配对象。它需要配合 ",[32,496,225],{"code":225}," 里的 ",[32,499,500],{"code":500},"failed"," 一起理解：当某个 chunk 的 ",[32,503,500],{"code":500}," 累计超过阈值，说明它已经反复装不下新请求了，",[32,506,493],{"code":493}," 便跳过它指向下一块；若后续没有可用块，就触发新 chunk 的分配。这样做的意义在于，分配时不必每次都从头遍历那些大概率已经填满的旧块。",[19,509,510,514,515,519],{},[45,511,512],{},[32,513,214],{"code":214}," 用于大内存分配，",[45,516,517],{},[32,518,217],{"code":217}," 用于资源清理，二者都是后文的主题。",[521,522,523],"blockquote",{},[19,524,525],{},"光看描述很懵，看代码会对这些概念有更清晰的认识。",[63,527],{},[10,529,531],{"id":530},"c-实现","C++ 实现",[19,533,534,535,537],{},"我对 ",[32,536,61],{"code":61}," 源码进行了 C++ 的一种重写，删减了一部分，但架构几乎一样。",[539,540,541],"quote",{},"如果你读懂下面的代码，那么读 Nginx 内存池源码自然水到渠成；反过来，如果你读过源码且有一定 C++ 基础，这就是一份项目上能用的 C++ 翻版 Nginx 内存池。",[87,543,544],{"id":544},"头文件",[19,546,547,548,551],{},"先展示代码，然后下文挑重点说。其余靠注释自行理解，将下文代码喂给 ",[32,549,550],{"code":550},"Claude code"," 是一个好的方式。",[181,553,558],{"className":554,"code":555,"filename":556,"language":98,"meta":557},[96],"class Pool : public runtime::base::NonCopyable {\npublic:\n  inline static constexpr std::size_t kDefaultChunkSize = 1 \u003C\u003C 12;\n  inline static constexpr std::size_t kMaxSmallAlloc = kDefaultChunkSize - 1;\n  inline static constexpr std::size_t kFailedThreshold = 1 \u003C\u003C 2;\n  inline static constexpr std::size_t kMinChunkSize = 1 \u003C\u003C 7;\n\n  struct Deleter {\n    void operator()(Pool* p) const noexcept;\n  };\n\n  using Ptr = std::unique_ptr\u003CPool, Deleter>;\n\n  \u002F\u002F Pool must be placement-new'ed at the beginning of its own arena memory,\n  \u002F\u002F so stack allocation and direct new are intentionally disallowed.\n  static Ptr Create(std::size_t chunk_size = kDefaultChunkSize);\n\n  \u002F\u002F size \u003C= max_ uses the bump arena fast path.\n  \u002F\u002F Larger allocations bypass the arena and use the large-allocation path.\n  void* Allocate(std::size_t size);\n  void* AllocateAligned(std::size_t size, std::size_t align);\n  void* AllocateUnaligned(std::size_t size);\n  void* Callocate(std::size_t size);\n\n  \u002F\u002F Only valid for large allocations.\n  \u002F\u002F Small allocations are reclaimed by Reset() or Pool destruction.\n  void Free(void* p) noexcept;\n\n  \u002F\u002F Does not execute cleanup handlers.\n  \u002F\u002F Releases large allocations and rewinds all chunk bump pointers.\n  void Reset() noexcept;\n\n  \u002F\u002F handler(data) is executed in LIFO order during Pool destruction.\n  \u002F\u002F Returned data memory is allocated from the arena itself.\n  void* RegisterCleanup(void (*handler)(void*), std::size_t data_size);\n\n  std::size_t ChunkCount() const noexcept;\n  std::size_t LargeCount() const noexcept;\n  std::size_t ByteUsed() const noexcept;\n\nprivate:\n  struct ChunkHeader {\n    std::byte* last;\n    std::byte* end;\n    ChunkHeader* next;\n    std::uint32_t failed;\n  };\n\n  struct LargeNode {\n    void* alloc;\n    LargeNode* next;\n  };\n\n  struct CleanupNode {\n    void (*handler)(void*);\n    void* data;\n    CleanupNode* next;\n  };\n\n  explicit Pool(std::size_t chunk_size) noexcept;\n  ~Pool() = default;\n\n  void DestroyArena() noexcept;\n  void* AllocateSmall(std::size_t size, std::size_t alignment);\n  void* AllocateLarge(std::size_t size);\n  ChunkHeader* AllocateChunk();\n\n  \u002F\u002F reinterpret_cast\u003CChunkHeader*>(this) == &d_\n  ChunkHeader d_;\n  std::size_t max_;\n  ChunkHeader* current_;\n  LargeNode* large_;\n  CleanupNode* cleanup_;\n};\n","pool.h","icon=tabler:braces",[32,559,555],{"__ignoreMap":191},[19,561,562],{},"下面挑四个真正影响设计的点说明，其余靠注释自解释。",[37,564,566,587,590,609,621],{"title":565,"type":40},"一、Create 工厂 + placement-new：Pool 住在自己的 arena 里",[19,567,568,569,572,573,576,577,580,581,583,584,78],{},"这是整个类最反直觉、也最关键的设计。",[32,570,571],{"code":571},"Pool"," 的构造函数是 ",[32,574,575],{"code":575},"private"," 的，唯一入口是静态的 ",[32,578,579],{"code":579},"Create","。原因在于：",[32,582,571],{"code":571}," 对象本身并不独立存在于某处，它就",[45,585,586],{},"坐落在它所管理的那块 arena 内存的开头",[19,588,589],{},"理解的要点是 C++ 申请原始字节与构造对象可视为两步：",[113,591,592,602],{},[116,593,594,598,599],{},[32,595,597],{"className":596,"code":597,"language":98},[96],"::operator new"," 对应 C 中的 ",[32,600,97],{"className":601,"code":97,"language":187},[184],[116,603,604,608],{},[32,605,607],{"className":606,"code":607,"language":98},[96],"placement-new"," 构造对象",[19,610,611,612,614,615,617,618,620],{},"对照 Nginx，首块 chunk 内嵌的是 ",[32,613,368],{"code":368}," 本体——C++ 版要复现这一点，就必须先 ",[32,616,597],{"code":597}," 出整块 arena，再用 placement-new 把 ",[32,619,571],{"code":571}," 构造在这块内存的起始地址上。",[19,622,623,624,627,628,630,631,634,635,638],{},"正因如此，栈分配和普通 ",[32,625,626],{"code":626},"new"," 都被刻意禁止：如果 ",[32,629,571],{"code":571}," 被分配在别处，它的 ",[32,632,633],{"code":633},"this"," 就不再是 arena 的起点，",[32,636,637],{"code":637},"d_.last = this + sizeof(Pool)"," 这套地址推算会整个失效。",[37,640,642,653,677,700],{"title":641,"type":40},"二、Ptr 与自定义 Deleter：析构路径不能交给默认行为",[19,643,644,645,648,649,652],{},"为什么要定义删除器 ",[32,646,647],{"code":647},"Deleter","，不是直接 ",[32,650,651],{"code":651},"delete","？",[19,654,655,656,658,659,661,662,664,665,668,669,673,674,676],{},"因为 ",[32,657,571],{"code":571}," 是 placement-new 出来的，它的销毁就不能走 ",[32,660,651],{"code":651},"——",[32,663,651],{"code":651}," 会同时调析构和 ",[32,666,667],{"code":667},"::operator delete","，但 placement-new 的对象内存不归它管。所以这里用 ",[32,670,672],{"className":671,"code":672,"language":98},[96],"std::unique_ptr\u003CPool, Deleter>"," 包装，",[32,675,647],{"code":647}," 里手动编排了正确的三步：",[110,678,679],{},[113,680,681,688,694],{},[116,682,683,684,687],{},"先 ",[32,685,686],{"code":686},"DestroyArena()"," 清理资源与后续 chunk",[116,689,690,691],{},"再显式调 ",[32,692,693],{"code":693},"~Pool()",[116,695,696,697,699],{},"最后才 ",[32,698,667],{"code":667}," 释放首块 arena",[19,701,702,703,705,706,709,710,78],{},"这套逻辑钉进 ",[32,704,647],{"code":647},"，使用者只需持有一个 ",[32,707,708],{"code":708},"Ptr","，RAII 自动兜底，无需手动",[45,711,712],{},"内存管理",[37,714,716,738],{"title":715,"type":40},"三、ChunkHeader 取代 ngx_pool_data_t，并复用 this == &d_",[19,717,718,721,722,725,726,729,730,734,735,737],{},[32,719,720],{"code":720},"d_"," 是 ",[32,723,724],{"code":724},"ChunkHeader"," 类型，且它是类的",[45,727,728],{},"第一个","数据成员，因此 ",[32,731,733],{"className":732,"code":733,"language":98},[96],"reinterpret_cast\u003CChunkHeader*>(this) == &d_"," 成立。这让首块在遍历时可以和其余 chunk 一视同仁地当作 ",[32,736,724],{"code":724}," 处理，省去为首块单独写一套逻辑。",[19,739,740,742,743,746,747,750],{},[32,741,500],{"code":500}," 字段从 Nginx 的 ",[32,744,745],{"code":745},"ngx_uint_t"," 收窄成了 ",[32,748,749],{"code":749},"std::uint32_t","——计数器不需要 64 位，顺便压一点结构体体积。",[37,752,754,772],{"title":753,"type":40},"四、三类节点分离：small \u002F large \u002F cleanup 各走各的链",[19,755,756,198,759,762,763,74,765,767,768,771],{},[32,757,758],{"code":758},"LargeNode",[32,760,761],{"code":761},"CleanupNode"," 被拆成独立的小结构体，分别串成两条链表，与 bump arena 的主链彻底解耦。这对应 Nginx 里 ",[32,764,214],{"code":214},[32,766,217],{"code":217}," 各自成链的设计：大内存被单独 ",[32,769,770],{"code":770},"Free","，清理回调需要按 后进先出 LIFO 触发。",[19,773,774,775,198,777,779],{},"侵入式链表结构的一个特征是所有权平行分离。",[32,776,758],{"code":758},[32,778,761],{"code":761}," 二者的生命周期语义都和\"只进不退\"的 bump 主链不同，混在一起会互相掣肘。",[63,781],{},[87,783,784],{"id":784},"源文件",[110,786,787],{},[113,788,789,797,814],{},[116,790,791,793,794,796],{},[32,792,579],{"code":579}," \u002F ",[32,795,647],{"code":647}," \u002F 构造函数对应的就是头文件讲过的\"placement-new 三步走\"，具体逻辑自行阅读",[116,798,799,800,793,803,793,806,809,810,813],{},"统计函数（",[32,801,802],{"code":802},"ChunkCount",[32,804,805],{"code":805},"LargeCount",[32,807,808],{"code":808},"ByteUsed","）和 ",[32,811,812],{"code":812},"Reset"," 都是直白的链表遍历与计数操作，看代码即可",[116,815,816,817],{},"真正值得展开的是两条分配路径——小对象走快路径分配，大内存路径单独 ",[32,818,97],{"code":97},[820,821,823,826],"h4",{"id":822},"allocatesmallbump-fast-path-的核心",[32,824,825],{"code":825},"AllocateSmall","：bump fast-path 的核心",[181,828,833],{"className":829,"code":830,"filename":831,"language":98,"meta":832},[96],"void* Pool::AllocateSmall(std::size_t size, std::size_t align) {\n  for (ChunkHeader* c = current_; \u002F* void *\u002F; c = c->next) {\n    std::byte* aligned = AlignPtr(c->last, align);\n    if (aligned \u003C= c->end &&\n        static_cast\u003Cstd::size_t>(c->end - aligned) >= size) {\n      c->last = aligned + size;\n      return aligned;\n    }\n    if (c->next == nullptr) break;\n  }\n\n  \u002F\u002F No existing chunk has enough space. Allocate a new chunk.\n  ChunkHeader* fresh = AllocateChunk();\n  std::byte* aligned = AlignPtr(fresh->last, align);\n  void* result = aligned;\n  fresh->last = aligned + size;\n\n  \u002F\u002F nginx-style heuristic:\n  \u002F\u002F increment failed counters for skipped chunks and\n  \u002F\u002F gradually advance current_ toward newer chunks.\n  ChunkHeader* walk = current_;\n  for (; walk->next != nullptr; walk = walk->next) {\n    if (walk->failed++ >= kFailedThreshold) {\n      current_ = walk->next;\n    }\n  }\n  walk->next = fresh;\n  return result;\n}\n","pool.cpp","icon=tabler:bolt",[32,834,830],{"__ignoreMap":191},[19,836,837,838,841,842,844,845,848,849,851,852,854,855,857],{},"快路径就是前半段的循环：从 ",[32,839,840],{"code":840},"current_"," 出发，对每个 chunk 先把 ",[32,843,132],{"code":132}," 按 ",[32,846,847],{"code":847},"align"," 对齐，再做一次边界检查——对齐后的地址不越过 ",[32,850,126],{"code":126},"、且剩余空间够 ",[32,853,408],{"code":408},"，就把 ",[32,856,132],{"code":132}," 向后推并返回。整个过程没有查找、没有空闲链表，这正是 bump allocation 快的根源。",[19,859,860],{},"慢路径在所有现有 chunk 都装不下时触发：分配一块新 chunk，从它身上划出内存。",[37,862,864,884],{"title":863,"type":160},"有意为之的实现选择",[19,865,866,867,869,870,873,874,876,877,880,881,883],{},"和 Nginx 原版略有不同。Nginx 是在分配前遍历的过程中递增 ",[32,868,500],{"code":500},"；我是在新 chunk 分配完成后，单独走一遍 ",[32,871,872],{"code":872},"walk"," 循环来递增沿途 chunk 的 ",[32,875,500],{"code":500},"，并在超过 ",[32,878,879],{"code":879},"kFailedThreshold"," 时把 ",[32,882,840],{"code":840}," 往后挪。",[19,885,886,887,889],{},"语义上效果一致——某个 chunk 反复装不下，就挪动 ",[32,888,840],{"code":840}," 到有效的位置，后续分配从更新的块起步——只是我把\"计数\"与\"快路径判断\"拆开了，快路径循环保持纯粹，只管分配。",[820,891,893,896],{"id":892},"allocatelarge越过-arena-的大内存路径",[32,894,895],{"code":895},"AllocateLarge","：越过 arena 的大内存路径",[181,898,902],{"className":899,"code":900,"filename":831,"language":98,"meta":901},[96],"void* Pool::AllocateLarge(std::size_t size) {\n  void* alloc = ::operator new(size);\n\n  std::size_t probe = 0;\n  for (LargeNode* l = large_; l != nullptr; l = l->next) {\n    if (l->alloc == nullptr) {\n      l->alloc = alloc;\n      return alloc;\n    }\n    if (++probe >= kLargeSlotSearch) break;\n  }\n\n  auto* node = static_cast\u003CLargeNode*>(\n      AllocateSmall(sizeof(LargeNode), alignof(LargeNode)));\n  node->alloc = alloc;\n  node->next = large_;\n  large_ = node;\n  return alloc;\n}\n","icon=tabler:database",[32,903,900],{"__ignoreMap":191},[19,905,906,907,910,911,913,914,916],{},"超过 ",[32,908,909],{"code":909},"max_"," 的请求直接 ",[32,912,597],{"code":597},"，绕过 bump arena——arena 是为小对象的密集分配设计的，大块内存塞进去会浪费可用空间，也破坏整体一次性释放的前提（大内存需要能被 ",[32,915,770],{"code":770}," 单独回收）。",[451,918,920,940],{":open":56,"title":919},"🔍 两个值得一提的细节",[19,921,922,925,926,929,930,932,933,936,937,939],{},[45,923,924],{},"其一","，新分配的指针不是无脑挂链：先探测链表前 ",[32,927,928],{"code":928},"kLargeSlotSearch"," 个节点，如果有被 ",[32,931,770],{"code":770}," 置空（",[32,934,935],{"code":935},"alloc == nullptr","）的槽位就直接复用，省一次 ",[32,938,758],{"code":758}," 分配。",[19,941,942,479,945,947],{},[45,943,944],{},"其二",[32,946,758],{"code":758}," 这个节点本身只有十几字节，让它也从 bump arena 里划出来——管理结构借住在它所管理对象的对立路径上，\"能省一次分配就省一次\"。",[19,949,950,951,953,954,957,958,960],{},"至于 ",[32,952,770],{"code":770},"，它只对大内存有效：遍历 ",[32,955,956],{"code":956},"large_"," 链找到匹配指针，",[32,959,667],{"code":667}," 后把槽位置空（留给上面的复用逻辑）。小对象不支持单独释放——这是 bump allocation 的固有代价，前文已经说过。",[63,962],{},[10,964,965],{"id":965},"小结",[19,967,968],{},"到这里，一个翻版 Nginx 内存池的 C++ 实现就完整了。",[37,970,972],{"title":971,"type":40},"一句话总结",[19,973,974,78],{},[45,975,976],{},"小对象只进不退、批量重置；大对象单独管理、单独释放",[19,978,979],{},"如上所说，我删减了 Nginx 内存池的一部分，另外的差异是语言层面的设计思路：",[110,981,982],{},[113,983,984,987,996,999],{},[116,985,986],{},"工厂模式设计，全堆分配",[116,988,989,992,993,995],{},[32,990,991],{"code":991},"unique_ptr"," + 自定义 ",[32,994,647],{"code":647}," 接管销毁路径",[116,997,998],{},"RAII 封装，无需手动管理内存",[116,1000,1001,1004],{},[32,1002,1003],{"code":1003},"std::byte"," 与显式内存对齐",[539,1006,1007],{},[19,1008,1009],{},"这种内存池适用于游戏引擎和编译器生成语法树，这些我只停留在描述上。但我可以肯定，它在 HTTP 路由解析和网关路由协议改写，是非常高效的。这也是我最初学习并优化它的原因。",[19,1011,1012,1013,1015,1016,1019,1020,1024,1025,1029,1030,1032],{},"另外，把它接入 C++17 的 ",[32,1014,34],{"code":34},"——它能作为标准的 ",[32,1017,1018],{"code":1018},"memory_resource"," 暴露出去，",[32,1021,1023],{"className":1022,"code":1023,"language":98},[96],"std::pmr::string","、",[32,1026,1028],{"className":1027,"code":1028,"language":98},[96],"std::pmr::vector"," 这些容器就能直接以它为分配源。C++ 也提供 ",[32,1031,61],{"code":61}," 风格的分配源，感兴趣自行了解吧。",[1034,1035,1036],"blur",{},"下一篇也许会写 std::pmr 的接入细节。",[63,1038],{},[10,1040,1042],{"id":1041},"参考附录-版权声明","参考附录 && 版权声明",[110,1044,1045],{},[113,1046,1047,1053],{},[116,1048,1049,1052],{},[45,1050,1051],{},"Nginx 源码","：经典中经典，必看，代码简洁优雅",[116,1054,1055,1058],{},[45,1056,1057],{},"Apache 源码","：Nginx 作者早期参考的经典，代码过长自行阅读",[113,1060,1061,1079,1086,1093,1100,1107],{},[116,1062,1063,1070,1071,1074,1075,1078],{},[1064,1065,1069],"a",{"href":1066,"rel":1067},"https:\u002F\u002Fgithub.com\u002Fakiba-miku\u002Fhigh-concurrency-runtime\u002Fblob\u002Fmain\u002Finclude\u002Fruntime\u002Fmemory\u002Fpool.h",[1068],"nofollow","我的实现 - 头文件"," 觉得不错的 ",[32,1072,1073],{"code":1073},"star"," 一下呗 ",[54,1076,1077],{":round":56},"Star"," 👋",[116,1080,1081],{},[1064,1082,1085],{"href":1083,"rel":1084},"https:\u002F\u002Fgithub.com\u002Fakiba-miku\u002Fhigh-concurrency-runtime\u002Fblob\u002Fmain\u002Fsrc\u002Fmemory\u002Fpool.cpp",[1068],"我的实现 - 源文件",[116,1087,1088],{},[1064,1089,1092],{"href":1090,"rel":1091},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FRegion-based_memory_management#",[1068],"Arena 和 Bump - Wikipedia",[116,1094,1095],{},[1064,1096,1099],{"href":1097,"rel":1098},"https:\u002F\u002Fwangziqi2013.github.io\u002Fpaper\u002F2021\u002F08\u002F22\u002Ffast-allocation-and-dealloaction.html",[1068],"C 语言 Arena 起源论文 - By D R.H",[116,1101,1102],{},[1064,1103,1106],{"href":1104,"rel":1105},"https:\u002F\u002Fgithub.com\u002Fnginx\u002Fnginx\u002Fblob\u002Fmaster\u002Fsrc\u002Fcore\u002Fngx_palloc.h",[1068],"Nginx 内存池源码",[116,1108,1109],{},[1064,1110,1113],{"href":1111,"rel":1112},"https:\u002F\u002Fgithub.com\u002Fapache\u002Fapr\u002Fblob\u002Ftrunk\u002Fmemory\u002Funix\u002Fapr_pools.c",[1068],"Apache 内存池源码",[181,1115,1121],{"className":1116,"code":1118,"language":1119,"meta":1120},[1117],"language-md","Nginx 内存池的 C++ 翻版：Arena + Bump，placement-new + RAII。\n","md","wrap",[32,1122,1118],{"__ignoreMap":191},{"title":191,"searchDepth":1124,"depth":1124,"links":1125},4,[1126,1128,1133,1137,1146,1147],{"id":12,"depth":1127,"text":12},2,{"id":67,"depth":1127,"text":67,"children":1129},[1130,1132],{"id":89,"depth":1131,"text":77},3,{"id":105,"depth":1131,"text":73},{"id":175,"depth":1127,"text":176,"children":1134},[1135,1136],{"id":225,"depth":1131,"text":225},{"id":368,"depth":1131,"text":368},{"id":530,"depth":1127,"text":531,"children":1138},[1139,1140],{"id":544,"depth":1131,"text":544},{"id":784,"depth":1131,"text":784,"children":1141},[1142,1144],{"id":822,"depth":1124,"text":1143},"AllocateSmall：bump fast-path 的核心",{"id":892,"depth":1124,"text":1145},"AllocateLarge：越过 arena 的大内存路径",{"id":965,"depth":1127,"text":965},{"id":1041,"depth":1127,"text":1042},[1149],"技术","2026-05-24 18:33:28","Nginx风格的内存池 C++实现",false,null,{"aside":1155,"slots":1158},[1156,1157],"toc","meta-aside-foo",{"aside-track":1159,"aside-foo":1175},{"props":1160,"type":7,"value":1162},{":card":56,"card":191,"title":1161},"point",[1163],[113,1164,1165,1166,1165,1169,1165,1172,1165],{},"\n",[116,1167,1168],{},"看懂 Nginx 内存池的内嵌式结构",[116,1170,1171],{},"用 C++ 重写 Arena + Bump allocator",[116,1173,1174],{},"placement-new + 自定义 Deleter 接管销毁路径",{"props":1176,"type":7,"value":1178},{":card":56,"card":191,"title":1177},"🌸 延伸阅读",[1179,1182],[19,1180,1181],{},"读完这篇可以继续看：",[113,1183,1165,1184,1165,1192,1165,1198,1165],{},[116,1185,1186,1191],{},[45,1187,1188],{},[32,1189,1190],{"code":1190},"std::pmr","：C++17 多态分配器，把内存池接入标准容器",[116,1193,1194,1197],{},[45,1195,1196],{},"tcmalloc \u002F jemalloc","：通用场景下的工业级分配器",[116,1199,1200,1203],{},[45,1201,1202],{},"Slab allocator","：Linux 内核的对象级缓存分配",true,"\u002F2026\u002Fnginx_c++",{"text":1207,"minutes":1208,"time":1209,"words":1210},"18 min read",17.695,1061700,3539,{"title":5,"description":1151},{"loc":1205},"posts\u002F2026\u002F内存池仿Nginx_C++实现",[57,61,1215],"MemoryPool","tech","FNEXqhiNnjbZwd3HTet6ShMj9rsswW4hccBKBAq2hA0",[1219,1224],{"title":1220,"path":1221,"stem":1222,"date":1223,"type":1216,"children":-1},"网络层(一) why reactor and one-loop per thread","\u002F2026\u002F()-why-reactor-and-one-loop-per-thread","posts\u002F2026\u002F网络层(一) why reactor and one-loop per thread","2026-05-19 10:27:27",{"title":1225,"path":1226,"stem":1227,"date":1228,"type":1216,"children":-1},"图论-广度优先遍历及其拓展","\u002F2026","posts\u002F2026\u002F[图论]-广度优先遍历及其拓展","2026-04-30 21:20:24",1779619501424]