這一天,是筆者在 Mongory C 擴充完成後的第一波震撼教育:既然都進了 C,為什麼還是比 plain Ruby 慢?數據擺在眼前,在初版 bridge 完成後,整體大約是純 Ruby 的三倍時間,雖然略優於原先的 Mongory-rb,但離「追平甚至超車 plain Ruby」的目標還差一截。這篇把「怎麼量」、「量到什麼」、「為什麼慢」與「先做哪些修正」講清楚,並為 Day 19、Day 20 的關鍵優化鋪路。
筆者採「可重現」與「可對照」為原則,拆成兩種查詢形態:
age >= 18
age >= 18 OR status == 'active'
資料集規模
GC 與分配觀測(可選)
快速示例(與 repo 內 examples/benchmark.rb
思路一致,略作化繁為簡)
records = Array.new(100_000) do
{ 'age' => [nil, rand(1..100)].sample, 'status' => %w[active inactive].sample }
end
# Plain Ruby
Benchmark.measure { records.select { |r| r['age'].is_a?(Numeric) && r['age'] >= 18 } }
# Mongory (Ruby)
builder = records.mongory.where(:age.gte => 18)
Benchmark.measure { builder.to_a }
# Mongory::CMatcher
matcher = Mongory::CMatcher.new(:age.gte => 18)
Benchmark.measure { records.select { |r| matcher.match?(r) } }
# Mongory::CQueryBuilder(C bridge builder)
builder = records.mongory.c.where(:age.gte => 18)
Benchmark.measure { builder.to_a }
說明:
or({ :age.gte => 18 }, { status: 'active' })
做相同對照。提醒:數據會因機器、Ruby 版本、資料分佈而異,此處重點在「相對關係」與「趨勢」。
將可疑來源分為兩層:橋接開銷與資料結構開銷。
結論:在筆者的量測中,「重複配置」與「鍵轉換」是最先該下手的兩個點,能以低風險改動換回顯著收益,更進一步的結構性提升(O(1) 取值)會在 Day 20 展開。
先做兩個「低風險、可回退」的修正:
修正 A:導入 pool reset(而非每輪重配)
scratch_pool->reset(scratch_pool)
,重用已分配的 chunk,避免反覆 malloc/free。修正 B:Key Cache Map(字串/符號緩存)
String
/Symbol
,配合 mark_list
確保 GC 可見,取值時不再重複生成臨時字串/符號。觀察到的效果(相對趨勢)
Net alive
成長趨勢消失或放緩。records = Array.new(100_000) { { 'age' => rand(1..100), 'status' => %w[active inactive].sample } }
# Plain Ruby
Benchmark.measure { records.select { |r| r['age'].is_a?(Numeric) && r['age'] >= 18 } }
# Mongory Ruby
builder = records.mongory.where(:age.gte => 18)
Benchmark.measure { builder.to_a }
# Mongory C
builder = records.mongory.c.where(:age.gte => 18)
Benchmark.measure { builder.to_a }
plain = records.count { |r| r['age'].is_a?(Numeric) && r['age'] >= 18 }
mongo = records.mongory.where(:age.gte => 18).count
cmong = records.mongory.c.where(:age.gte => 18).count
raise 'count mismatch' unless plain == mongo && mongo == cmong
def gc_probe
GC.disable
before = GC.stat
yield
GC.start
after = GC.stat
GC.enable
puts({
created: after[:total_allocated_objects] - before[:total_allocated_objects],
freed: after[:total_freed_objects] - before[:total_freed_objects],
alive: after[:heap_live_slots] - before[:heap_live_slots]
})
end