perf(memhub): 分配记录表改为仅 --detection 使用,free 不再线性扫描全部历史分配

记录区此前与 detection 解耦、每次 alloc 都写一条,_det_count 对永不 reset 的 PermPool 无上限增长;free 归还时按 _det_count 线性查找记录并置空闲位,于是每次 free 都是 O(历史分配数)。改为 detection==0 时不分配记录区也不记录/不回填,free 变 O(1);OOM 报告的块序分布走 chk_stack 不受影响。
This commit is contained in:
TermiNexus committed 2026-10-24 13:31:51 +08:00
1 parent 5c0a454861
commit 963dfc5a3c
1 file changed
+33 -25
+33 -25
View File
@@ -765,16 +765,16 @@ class MemBuddy(MemManager):
rem -= blk
self.usable_size = used_area
# Allocate the record area separately with stdlib.malloc (not from the buddy area, same
# policy as chk_stack). Capacity = detection_size // 16 (each entry {ptr, size}).
# Records + overlap detection are decoupled from detection: even without it the record area
# is allocated and same-address duplicate-allocation detection is on (no markers, no layout
# change), which locates free-list / duplicate-allocation corruption.
det_arena: rawptr = stdlib.malloc(self.detection_size)
if det_arena is not None:
self._det_arena = det_arena
self._det_records = uint64*(det_arena)
self._det_cap = self.detection_size // 16
# Record area: allocated only in detection mode. Recording every alloc in production was
# 16 B per allocation (growing by realloc to hundreds of MB under churn) and made free's
# record-marking loop a linear scan of every allocation ever made -- unbounded for a pool
# that never resets (PermPool). All consumers already guard on `_det_records is not None`.
if self.detection != 0:
det_arena: rawptr = stdlib.malloc(self.detection_size)
if det_arena is not None:
self._det_arena = det_arena
self._det_records = uint64*(det_arena)
self._det_cap = self.detection_size // 16
self._det_count = 0
# === Checkpoint support ===
@@ -1602,18 +1602,24 @@ class MemBuddy(MemManager):
stdio.fflush(None)
stdlib.exit(1)
dbg_cur = rawptr(*((uint64*)(dbg_cur)))
if self._det_count >= self._det_cap:
# Record table full: double it so is_alive/_det_probe never miss live blocks.
new_cap: sizet = self._det_cap * 2
new_rec: rawptr = rawptr(stdlib.realloc(self._det_arena, new_cap * 16))
if new_rec is not None:
self._det_arena = new_rec
self._det_records = (uint64*)(new_rec)
self._det_cap = new_cap
if self._det_count < self._det_cap:
self._det_records[self._det_count * 2] = rec_ptr_d
self._det_records[self._det_count * 2 + 1] = uint64(size)
self._det_count += 1
# Records exist for the detection probes only ([DET] overlap / is_alive / 0xCD
# use-after-free fill) and for the record-mirror line of the OOM report. Keeping them
# in production costs 16 B per alloc AND makes free's record-marking loop O(_det_count)
# -- a linear scan of every allocation since the last reset, i.e. unbounded for a pool
# that never resets (PermPool). So record only when detection is on.
if self.detection != 0:
if self._det_count >= self._det_cap:
# Record table full: double it so is_alive/_det_probe never miss live blocks.
new_cap: sizet = self._det_cap * 2
new_rec: rawptr = rawptr(stdlib.realloc(self._det_arena, new_cap * 16))
if new_rec is not None:
self._det_arena = new_rec
self._det_records = (uint64*)(new_rec)
self._det_cap = new_cap
if self._det_count < self._det_cap:
self._det_records[self._det_count * 2] = rec_ptr_d
self._det_records[self._det_count * 2 + 1] = uint64(size)
self._det_count += 1
# Probe decision: never entered when detection is off; with it on, deep_detect
# probes every alloc, otherwise the historical sampling is used.
if self.detection != 0 and ((self._det_count & (MEMBUDDY_DET_PROBE_PERIOD - 1)) == 0 or self.deep_detect != 0):
@@ -1722,9 +1728,11 @@ class MemBuddy(MemManager):
if self.detection != 0:
# [DET] Returned ptr is 2 bytes past the user area; step back on free.
p_eff = ptr - 2
# Mark the record free (independent of detection): otherwise a later alloc of the
# same address (normal reuse) would be misreported as an overlap.
if self._det_records is not None:
# Mark the record free so a later alloc of the same address (normal reuse) is not
# misreported as an overlap. The record table is filled only in detection mode (see
# alloc), so this O(_det_count) scan runs only there: a pool that never resets
# (PermPool) otherwise paid a scan of every allocation ever made on each free.
if self.detection != 0 and self._det_records is not None:
# Read this record's real size (detection fills 0xCD after free, so a later
# write-through breaks the fill = use-after-free). Keep the record and set the
# free bit; the probe checks the 0xCD fill of still-free blocks.