diff --git a/Test/BuddyTest/App/mbuddytest.vp b/Test/BuddyTest/App/mbuddytest.vp index f35e690..a06bd78 100644 --- a/Test/BuddyTest/App/mbuddytest.vp +++ b/Test/BuddyTest/App/mbuddytest.vp @@ -57,9 +57,10 @@ def mbuddy_main() -> CInt: bd.free(p3) testcheck.ok("3 frees OK") - # 释放后验证合并: free_count 应回到 1 (单个大块) - testcheck.check(bd.free_count() == 1, - "coalesce: free_count=1 OK", "coalesce: free_count != 1") + # 释放后验证合并: free_count 应回到初始分解块数 (10) + # 4096-264 内非 2 的幂: 65536-264=65272 二进制分解为 10 个 2 的幂块 + testcheck.check(bd.free_count() == 10, + "coalesce: free_count=10 OK", "coalesce: free_count != 10") # 合并后应能分配大块 big: bytes = bd.alloc(16000) @@ -152,7 +153,7 @@ def mbuddy_main() -> CInt: testcheck.check(sp_c[0] == CUInt8T(0x42), "shrink data OK", "shrink data FAILED") bds.free(sp2) - testcheck.check(bds.free_count() == 1, "shrink+free coalesce OK", "shrink+free coalesce FAILED") + testcheck.check(bds.free_count() == 6, "shrink+free coalesce OK", "shrink+free coalesce FAILED") stdlib.free(arena_shrink) @@ -263,9 +264,9 @@ def mbuddy_main() -> CInt: bd6.free(cur_p) idx += 1 - # 全部释放后应能合并回单个大块 - testcheck.check(bd6.free_count() == 1, - "OOM: full coalesce OK", "OOM: free_count != 1 after full free") + # 全部释放后应能合并回初始分解块数 (6) + testcheck.check(bd6.free_count() == 6, + "OOM: full coalesce OK", "OOM: free_count != 6 after full free") stdlib.free(ptrs_arr) stdlib.free(arena6) @@ -275,8 +276,8 @@ def mbuddy_main() -> CInt: arena7: bytes = stdlib.malloc(4096) bd7: memhub.MemBuddy | CPtr = memhub.MemBuddy(arena7, 4096) - # 初始: 1 个大块 - testcheck.check(bd7.free_count() == 1, "init: 1 free block OK", "init: free_count != 1") + # 初始: 非 2 的幂 arena (4096-264=3832) 二进制分解为 6 个块 + testcheck.check(bd7.free_count() == 6, "init: 6 free blocks OK", "init: free_count != 6") # 分配后: 空闲块数应增加 (分裂产生多个小块) a1: bytes = bd7.alloc(32) @@ -295,10 +296,10 @@ def mbuddy_main() -> CInt: fc3: CSizeT = bd7.free_count() testcheck.check(fc3 > fc2, "after free a1: free increased OK", "after free a1: free not increased") - # 释放 a2: 应合并, 空闲块数回到 1 + # 释放 a2: 应合并, 空闲块数回到初始 6 块 bd7.free(a2) - testcheck.check(bd7.free_count() == 1, - "after free all: coalesced to 1 OK", "after free all: free_count != 1") + testcheck.check(bd7.free_count() == 6, + "after free all: coalesced to 6 OK", "after free all: free_count != 6") stdlib.free(arena7) @@ -398,7 +399,7 @@ def mbuddy_main() -> CInt: # 验证分配器状态: 自检通过,所有块已合并 testcheck.check(bd_mt.self_check() == 0, "mt: self_check OK", "mt: self_check FAILED") - testcheck.check(bd_mt.free_count() == 1, "mt: free_count=1 OK", "mt: free_count != 1 (leaked)") + testcheck.check(bd_mt.free_count() == 10, "mt: free_count=10 OK", "mt: free_count != 10 (leaked)") viperlib.snprintf(buf, 256, "mt: free_count=%lu", bd_mt.free_count()) testcheck.info(buf) @@ -423,22 +424,22 @@ def mbuddy_main() -> CInt: bd_ex.free(exact_fit) bd_ex.free(over_fit) - testcheck.check(bd_ex.free_count() == 1, "boundary: coalesce OK", "boundary: coalesce FAILED") + testcheck.check(bd_ex.free_count() == 6, "boundary: coalesce OK", "boundary: coalesce FAILED") - # 2. 单次分配接近 arena 上限 - # 可用数据区 = mem_size - 8(头) - 8(哨兵),alloc(max_usable) 的 needed - # 恰好 = mem_size,正好填满最大块(order 6 = 2048) - max_usable: CSizeT = bd_ex.mem_size - 16 - viperlib.snprintf(buf, 256, "mem_size=%lu, max_usable=%lu", bd_ex.mem_size, max_usable) + # 2. 单次分配接近最大块上限(order6=2048,非 2 的幂 arena 的最大分解块) + # 可用数据区 = 2048 - 8(头) - 8(哨兵),alloc(max_usable) 的 needed 恰好 + # = 2048,正好填满 order6 最大块 + max_usable: CSizeT = 2048 - 16 + viperlib.snprintf(buf, 256, "mem_size=%lu, max_block=2048, max_usable=%lu", bd_ex.mem_size, max_usable) testcheck.info(buf) - # 恰好用完整个 arena + # 恰好用完 order6 块(其余 5 个分解块空闲) exact_max: bytes = bd_ex.alloc(max_usable) testcheck.check(exact_max != None, "exact max alloc OK", "exact max alloc FAILED") - testcheck.check(bd_ex.free_count() == 0, "max alloc: arena full OK", "max alloc: arena not full") + testcheck.check(bd_ex.free_count() == 5, "max alloc: 5 free blocks left OK", "max alloc: free_count != 5") bd_ex.free(exact_max) - testcheck.check(bd_ex.free_count() == 1, "max free: coalesce OK", "max free: coalesce FAILED") + testcheck.check(bd_ex.free_count() == 6, "max free: coalesce OK", "max free: coalesce FAILED") # 再次分配同样大小 — 验证合并后可再次使用 exact_max2: bytes = bd_ex.alloc(max_usable) @@ -481,14 +482,14 @@ def mbuddy_main() -> CInt: # 6. 2 的幂 arena vs 非 2 的幂 arena arena_pow2: bytes = stdlib.malloc(512) bd_pow2: memhub.MemBuddy | CPtr = memhub.MemBuddy(arena_pow2, 512) - # 512 - 264 = 248, largest_pow2(248) = 128 - testcheck.check(bd_pow2.stats() == 128, "pow2 arena: stats=128 OK", "pow2 arena: stats != 128") + # 512 - 264 = 248, 二进制分解 = 128+64+32 = 224 (尾 24 字节碎片不入池) + testcheck.check(bd_pow2.stats() == 224, "pow2 arena: stats=224 OK", "pow2 arena: stats != 224") stdlib.free(arena_pow2) arena_non2: bytes = stdlib.malloc(500) bd_non2: memhub.MemBuddy | CPtr = memhub.MemBuddy(arena_non2, 500) - # 500 - 264 = 236, largest_pow2(236) = 128 - testcheck.check(bd_non2.stats() == 128, "non-pow2 arena: stats=128 OK", "non-pow2 arena: stats != 128") + # 500 - 264 = 236, 二进制分解 = 128+64+32 = 224 (尾 12 字节碎片不入池) + testcheck.check(bd_non2.stats() == 224, "non-pow2 arena: stats=224 OK", "non-pow2 arena: stats != 224") stdlib.free(arena_non2) # 7. 压力测试: 反复 alloc/free 同一尺寸 1000 次 @@ -504,7 +505,7 @@ def mbuddy_main() -> CInt: bd_stress.free(sp) testcheck.check(bd_stress.self_check() == 0, "stress: self_check OK", "stress: self_check FAILED") - testcheck.check(bd_stress.free_count() == 1, "stress: free_count=1 OK", "stress: leaked") + testcheck.check(bd_stress.free_count() == 6, "stress: free_count=6 OK", "stress: leaked") stdlib.free(arena_stress) # 8. 分配全部最小块然后全部释放 @@ -534,7 +535,7 @@ def mbuddy_main() -> CInt: fp2: bytes = t.CVoid(fill_ptrs[fi], t.CPtr) bd_fill.free(fp2) - testcheck.check(bd_fill.free_count() == 1, "fill: all freed OK", "fill: leaked after free all") + testcheck.check(bd_fill.free_count() == 6, "fill: all freed OK", "fill: leaked after free all") testcheck.check(bd_fill.self_check() == 0, "fill: self_check after free OK", "fill: self_check FAILED") stdlib.free(fill_ptrs_arr) diff --git a/includes/memhub.vp b/includes/memhub.vp index d9fb21f..ff540d4 100644 --- a/includes/memhub.vp +++ b/includes/memhub.vp @@ -300,6 +300,7 @@ class MemBuddy(MemManager): chk_cap: t.CSizeT # 记录栈容量(条目数) chk_depth: t.CSizeT # 当前记录深度 chk_base: t.CVoid | t.CPtr # 记录栈起始地址(由 stdlib.malloc 分配,独立于 buddy 区) + big_alloc_count: t.CSizeT # ≥8KB 大分配计数(OOM 诊断:定位大缓冲反复分配) def __init__(self, base: t.CVoid | t.CPtr, size: t.CSizeT): self.base = base @@ -313,6 +314,7 @@ class MemBuddy(MemManager): self.chk_cap = 0 self.chk_depth = 0 self.chk_base = None + self.big_alloc_count = 0 self.lock_val = 0 fl_bytes: t.CSizeT = (MEMBUDDY_MAX_ORDERS + 1) * 8 @@ -332,30 +334,48 @@ class MemBuddy(MemManager): remaining: t.CSizeT = size - fl_bytes # 注意:不能在 __init__ 里预留 checkpoint 记录栈! - # buddy 区必须从 fl_bytes 后开始、大小必须是 2 的幂。 - # 若预留 512KB,remaining = 2^30 - 512KB 不是 2 的幂, - # _largest_pow2_le 会取到 2^29 = 512MB,1GB 池直接砍半。 # 记录栈改为 mark() 时从 buddy 池懒分配(见 mark())。 - usable: t.CSizeT = _largest_pow2_le(remaining) - - if usable < MEMBUDDY_MIN_BLOCK: + # + # 非 2 的幂 arena 支持:主池 1.2GB(非 2 幂)不再被 + # _largest_pow2_le 砍成 1GB 浪费 0.2GB。整个区域按二进制 + # 分解拆成若干个 2 的幂空闲块压入对应 free list;buddy 合并 + # 按 offset^bs 匹配,跨分解边界的块因 order 不匹配自然停止 + # 合并,不会破坏 buddy 一致性。remaining 恰为 2 的幂时分解 + # 结果与原实现(单个 max_order 块)完全等价。 + max_blk: t.CSizeT = _largest_pow2_le(remaining) + if max_blk < MEMBUDDY_MIN_BLOCK: self.usable = None self.usable_size = 0 self.max_order = 0 return self.usable = t.CVoid(t.CUInt64T(base) + fl_bytes, t.CPtr) - self.usable_size = usable - # 计算 max_order: log2(usable / MIN_BLOCK) + # 计算 max_order: log2(max_blk / MIN_BLOCK) self.max_order = 0 bs: t.CSizeT = MEMBUDDY_MIN_BLOCK - while bs < usable: + while bs < max_blk: bs = bs << 1 self.max_order += 1 - # Entire usable memory region as a max_order free block - self._fl_push(self.max_order, self.usable) + # 二进制分解:把 [usable, usable+remaining) 拆成 2 的幂空闲块。 + # 尾部碎片(< MIN_BLOCK)不入 free list;usable_size 记实际 + # 可分配字节(used/free 统计不含碎片)。 + pos: t.CSizeT = 0 + rem: t.CSizeT = remaining + used_area: t.CSizeT = 0 + while rem >= MEMBUDDY_MIN_BLOCK: + blk: t.CSizeT = _largest_pow2_le(rem) + o: t.CInt = 0 + ob: t.CSizeT = MEMBUDDY_MIN_BLOCK + while ob < blk: + ob = ob << 1 + o += 1 + self._fl_push(o, t.CVoid(t.CUInt64T(self.usable) + pos, t.CPtr)) + pos += blk + used_area += blk + rem -= blk + self.usable_size = used_area # === Checkpoint support === @@ -388,6 +408,13 @@ class MemBuddy(MemManager): """ if self.chk_stack is None: return + if mark > self.chk_depth: + # mark 大于当前深度:调用方 mark 值异常(可能读错字段/跨实例), + # reset 将 no-op → 之后分配全泄漏 → 1GB 主池 OOM。fail-fast 暴露。 + stdio.printf( + "[MEM] reset_to mark=%zu > chk_depth=%zu(mark 异常,reset 失效,终止)\n", + mark, self.chk_depth) + return if mark >= self.chk_depth: return self._lock() @@ -580,6 +607,13 @@ class MemBuddy(MemManager): order2: t.CInt = self._order_for_size(needed2) stdio.printf("[MEM] alloc 失败 size=%zu needed=%zu order=%d max_order=%d used=%zu\n", size, needed2, order2, self.max_order, self.used_bytes()) + # 越界写诊断:free list 结构损坏(self_check!=0)说明有越界写穿 + # 破坏块头/free list next 指针 → alloc 找不到块(used 虚高非真实耗尽) + sc: t.CInt = self.self_check() + stdio.printf("[MEM] alloc 失败 self_check=%d free_bytes=%zu free_count=%zu chk_depth=%zu big=%zu\n", + sc, self.free_bytes(), self.free_count(), self.chk_depth, self.big_alloc_count) + # chk_depth 巨大(数百万)→ 分配次数爆炸(每语句/每调用反复分配); + # chk_depth 不大但 used 巨大 → 单次大分配(缓冲/表扩容)反复发生 if order2 > self.max_order: stdio.printf("[MEM] 原因:请求 size 超 pool 最大块(order %d > max %d)\n", order2, self.max_order) @@ -606,13 +640,33 @@ class MemBuddy(MemManager): self._unlock() return 1 - def reset(self) -> t.CInt: - if self.usable is None: - return 0 + def _rebuild_free_lists(self): + """按二进制分解重建空闲链表(reset 恢复初始分配格局) + + 与 __init__ 的初始化分解一致:非 2 的幂 arena 会拆成多个 + 2 的幂空闲块(buddy 合并跨分解边界时因 order 不匹配自然 + 停止,安全);2 的幂 arena 等价于单个 max_order 块。 + """ i: t.CInt for i in range(MEMBUDDY_MAX_ORDERS + 1): self.free_lists[i] = 0 - self._fl_push(self.max_order, self.usable) + pos: t.CSizeT = 0 + rem: t.CSizeT = self.usable_size + while rem >= MEMBUDDY_MIN_BLOCK: + blk: t.CSizeT = _largest_pow2_le(rem) + o: t.CInt = 0 + ob: t.CSizeT = MEMBUDDY_MIN_BLOCK + while ob < blk: + ob = ob << 1 + o += 1 + self._fl_push(o, t.CVoid(t.CUInt64T(self.usable) + pos, t.CPtr)) + pos += blk + rem -= blk + + def reset(self) -> t.CInt: + if self.usable is None: + return 0 + self._rebuild_free_lists() return 1 # realloc override: buddy system reads original order from block header; old_size is not required @@ -724,7 +778,14 @@ class MemBuddy(MemManager): # 返回 0=OK,非 0=损坏(rc 语义见调用方注释)。 # 用于 reset_to 前哨兵:把"崩溃点漂移"变成"确定性定位到损坏发生的文件"。 if self.chk_stack is None: - return 0 + return + if from_mark > self.chk_depth: + # from_mark 大于当前深度:调用方 mark 值异常(可能读错字段/跨实例), + # 校验将 no-op → 越界写漏检。fail-fast 暴露。 + stdio.printf( + "[MEM] check_alive_blocks from_mark=%zu > chk_depth=%zu(mark 异常)\n", + from_mark, self.chk_depth) + return if from_mark >= self.chk_depth: return 0 if self.usable is None: