iT邦幫忙

2025 iThome 鐵人賽

DAY 17
0
佛心分享-SideProject30

Mongory:打造跨語言、高效能的萬用查詢引擎系列 第 18

Day 17:GC × memory pool 生命週期管理

  • 分享至 

  • xImage
  •  

本篇聚焦 Ruby bridge 與 C Core 之間的生命週期協作:memory pool 如何與 Ruby GC 配合,extern_ctx 如何穩定保存外部狀態,以及如何避免資源洩漏。筆者將以 Mongory 的實作為藍本,整理設計原則、落地策略與常見陷阱。

名詞釐清

  • memory pool:
    • matcher pool:建構與保存 matcher、條件樹、緩存(字串/符號)等長生命週期資產。
    • scratch pool:一次匹配(match/explain)中的暫態記憶體,完成即 reset
    • trace pool:開啟 trace 時使用,輸出後重置或釋放。
  • origin:mongory_value.origin 指向外部(Ruby VALUE)原始物件,供 recover 與 GC 標記。
  • mark_list:Ruby 端為了協同 GC,在 wrapper 內保存一份「需被標記」的列表。
  • extern_ctx:對應 Ruby 側的上下文(例如使用者傳入的 context),會被安全地橋接至 C,再傳回到自定義 custom matcher 做使用。

生命週期模型(高層)

  1. 構建期(build)
  • 建立 matcher pool、scratch pool;
  • condition 走 deep 轉換,AST 常駐 matcher pool;
  • 設定 extern_ctx 與 key 緩存(string_map/symbol_map),並掛入 mark_list
  1. 匹配期(match/explain/trace)
  • data 走 shallow(零拷貝);
  • scratch pool 用完即 reset
  • trace 開啟則使用 trace pool,輸出後再 reset/free
  1. 終結期(wrapper 釋放)
  • 釋放 matcher pool、scratch pool、trace pool;
  • 由 Ruby GC 回收 wrapper 與其持有之 Ruby 物件(已被正確標記)。

Ruby × pool:建立與釋放

Ruby 端透過自有結構包一層 C 的 pool,生命週期由 wrapper 控制:

static rb_mongory_memory_pool_t *rb_mongory_memory_pool_new() {
  rb_mongory_memory_pool_t *pool = malloc(sizeof(rb_mongory_memory_pool_t));
  mongory_memory_pool *base = mongory_memory_pool_new();
  memcpy(&pool->base, base, sizeof(mongory_memory_pool));
  free(base);
  return pool;
}

釋放時,三種 pool 依序釋放,避免殘留:

static void rb_mongory_matcher_free(void *ptr) {
  rb_mongory_matcher_t *wrapper = (rb_mongory_matcher_t *)ptr;
  mongory_memory_pool *pool = wrapper->pool;
  mongory_memory_pool *scratch_pool = wrapper->scratch_pool;
  mongory_memory_pool *trace_pool = wrapper->trace_pool;
  pool->free(pool);
  scratch_pool->free(scratch_pool);
  if (trace_pool) {
    trace_pool->free(trace_pool);
  }
  xfree(wrapper);
}

Ruby GC 協同:mark 與 origin

核心原則:C 側永不直接管理 Ruby 物件的生命週期;僅保存其 VALUE 至 origin,並確保 Ruby GC 能標記到。

static bool gc_mark_array_cb(mongory_value *value, void *acc) {
  (void)acc;
  if (value && value->origin) rb_gc_mark((VALUE)value->origin);
  return true;
}

static void rb_mongory_matcher_mark(void *ptr) {
  rb_mongory_matcher_t *self = (rb_mongory_matcher_t *)ptr;
  if (!self) return;
  self->mark_list->each(self->mark_list, NULL, gc_mark_array_cb);
}

落地策略:

  • 任何從 Ruby 來的值(key、Regex、custom matcher 實體、context)都需以 mongory_value 包裝,並掛入 mark_list
  • origin 指回 Ruby VALUE,協助 recover 與 GC;禁止在 C 側複製 Ruby 物件內容。

extern_ctx 與外部物件壽命

extern_ctx 是由 Ruby 使用者傳入的上下文對象,需伴隨 matcher 的整體生命週期:

static void rb_mongory_matcher_parse_argv(rb_mongory_matcher_t *self, int argc, VALUE *argv) {
  VALUE condition, kw_hash;
  rb_scan_args(argc, argv, "1:", &condition, &kw_hash);
  const ID ctx_id[1] = { rb_intern("context") };
  VALUE kw_vals[1] = { Qundef };
  if (kw_hash != Qnil) {
    rb_get_kwargs(kw_hash, ctx_id, 1, 0, kw_vals);
  }
  if (kw_vals[0] != Qundef) {
    self->ctx = kw_vals[0];
  } else {
    self->ctx = rb_funcall(cMongoryMatcherContext, rb_intern("new"), 0);
  }
  VALUE converted_condition = rb_funcall(inMongoryConditionConverter, rb_intern("convert"), 1, condition);
  self->condition = rb_to_mongory_value_deep(self->pool, converted_condition);
  self->mark_list->push(self->mark_list, self->condition);
  mongory_value *store_ctx = mongory_value_wrap_u(self->pool, (void *)self->ctx);
  store_ctx->origin = (void *)self->ctx;
  self->mark_list->push(self->mark_list, store_ctx);
}

重點:

  • self->ctx 以 VALUE 保存於 wrapper,同步再以 mongory_value 包裝存入 matcher pool,並加入 mark_list
  • 任何需要跨邊界持有的 Ruby 物件,都應採同樣套路(包裝 → 設置 origin → push 進 mark_list)。

scratch/trace:短生命週期與 reset

  • match?:data 走 shallow,分配於 scratch pool;執行完立即 reset,確保不殘留。
  • trace:建立臨時 trace pool,印出後釋放;或 enable_trace 後重複利用但每次 reset
static VALUE rb_mongory_matcher_match(VALUE self, VALUE data) {
  rb_mongory_matcher_t *self_wrapper;
  TypedData_Get_Struct(self, rb_mongory_matcher_t, &rb_mongory_matcher_type, self_wrapper);
  mongory_matcher *matcher = self_wrapper->matcher;
  mongory_memory_pool *scratch_pool = self_wrapper->scratch_pool;
  mongory_memory_pool *trace_pool = self_wrapper->trace_pool;
  mongory_value *data_value = rb_to_mongory_value_shallow(scratch_pool, data);
  if (rb_mongory_error_handling(scratch_pool, "Match failed")) {
    return Qnil;
  }
  bool result = mongory_matcher_match(matcher, data_value);
  if (trace_pool) {
    mongory_matcher_print_trace(matcher);
    trace_pool->reset(trace_pool);
    mongory_matcher_enable_trace(matcher, trace_pool);
  }
  scratch_pool->reset(scratch_pool);
  return result ? Qtrue : Qfalse;
}

資源洩漏與回收策略

  • 一致性原則:所有 C 側配置(含中間字串、包裝節點)一律綁定某個 pool。
  • 外部資源追蹤:若需持有非 pool 分配的記憶體(如外部函式庫回傳),使用 pool 的 trace 記錄,以在 free 時一併處理。
  • 價值物件保存:任何 Ruby 來源的值務必 origin 回填並 push 至 mark_list
  • pool 範疇清晰:build→matcher pool;一次性工作 →scratch/trace pool;避免交叉存放導致提前釋放或懸掛指標。

常見陷阱(必讀)

  • 忘記 push 到 mark_list

    • 現象:Ruby 物件未被 GC 標記,導致 origin 懸掛;
    • 解法:凡是從 Ruby 來、需跨界持有的值,都以 mongory_value 包裝並 push。
  • reset 後仍持有 shallow 值

    • 現象:指向 scratch pool 的值在下一輪已被重用;
    • 解法:僅在匹配過程中使用 shallow 值,必要時轉為 deep(放進 matcher pool)。
  • 在 converter 做行為邏輯

    • 現象:生命週期錯配、責任混淆;
    • 解法:行為(Regex、Custom Matcher)一律走 adapter,converter 只管形狀與封裝。
  • 忽略 trace_pool 的 reset/free

    • 現象:多次 trace 後記憶體增長;
    • 解法:印出後 reset,不再使用則 disable_trace + 釋放。

檢查清單(Cheat Sheet)

  • extern_ctx:包裝成 mongory_value,設置 origin,push 至 mark_list
  • condition:deep 轉換進 matcher pool,AST 穩定可重用。
  • data:shallow 包裝於 scratch pool,匹配後 reset
  • trace:需要時建立/啟用,使用後 reset/free
  • free:wrapper 釋放時,依序 free 三類 pool。

實戰步驟(Ruby)

# 1) 建立 CMatcher:condition deep;extern_ctx 保存,並掛入 mark_list
matcher = Mongory::CMatcher.new({ age: { "$gt" => 30 } }, context: { tenant_id: "t1" })

# 2) 匹配:data shallow;scratch_pool 事後 reset
data = { name: "Alice", age: 31 }
matched = matcher.match?(data)

# 3) 觀測:啟用 trace,逐次輸出並重置 trace_pool
matcher.enable_trace
matcher.trace(data)
matcher.print_trace

收束

GC × memory pool 的協同關鍵有三:以 pool 管控記憶體、以 origin/mark_list 協同 Ruby GC、以 extern_ctx 穩定承載外部語境

把責任邊界釐清(資料給 converter、行為給 adapter),搭配明確的池化規則與檢查清單,就能在高效與安全間取得平衡。

下一篇預告

  • Benchmark shock:為什麼還是比 plain Ruby 慢?
    • 時間約 plain Ruby 三倍,略優於原 Mongory-rb 但還不夠
    • 為何比 plain Ruby 慢:pool reallocate(先概述,細節留到 Day19)
    • 量測方法與瓶頸定位
    • 實驗與修正

專案首頁(Ruby 版)


上一篇
Day 16:VALUE↔C value 轉換策略
下一篇
Day 18:Benchmark shock:為什麼還是比 plain Ruby 慢?
系列文
Mongory:打造跨語言、高效能的萬用查詢引擎25
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言