Week 2 正式開始
筆者在把 Mongory 介紹給公司同事時,同事冷不防一句:「如果他能比純 Ruby 快,我才會想用它」
於是我開始了 Benchmark 測試
畢竟推向實戰時,第一個要面對的問題就是效能:純 Ruby 的 filter 與 Mongory 相比到底如何?
本篇先釐清「要比什麼、怎麼比、怎麼看」,並給出可直接複製的基準腳本,為後續 C Core 與 Bridge 篇鋪路
Array#select
+ 區塊)效能高,但不好維護srand
)require 'benchmark'
require 'json'
require 'securerandom'
require 'mongory'
Mongory.enable_symbol_snippets!
Mongory.register(Array)
def gen_records(n)
srand(42)
Array.new(n) do |i|
{
'id' => i,
'age' => 15 + rand(15),
'name' => %w[Ann Ben Cody Jack Jill Mary Bob].sample,
'status' => %w[active pending inactive].sample,
'tags' => [
{ 'name' => %w[ruby rails go js py].sample, 'priority' => 1 + rand(10) }
]
}
end
end
def plain_filter(records)
records.select do |r|
r['age'] >= 18 && (
r['name'] =~ /^J/ || r['tags'].any? { |t| t['name'] == 'ruby' && t['priority'] > 5 }
)
end
end
def mongory_filter_rb(records)
q = records.mongory
.where(:age.gte => 18)
.any_of({ :name.regex => /^J/ }, { :tags.elem_match => { :name => 'ruby', :priority.gt => 5 } })
q.to_a
end
def bench_once(size)
records = gen_records(size)
# 正確性先驗
a = plain_filter(records)
b = mongory_filter_rb(records)
raise 'mismatch (rb)' unless a == b
GC.start
puts "\n== size=#{size}"
Benchmark.bm(14) do |x|
x.report('plain_ruby') { 3.times { plain_filter(records) } }
x.report('mongory_rb') { 3.times { mongory_filter_rb(records) } }
end
end
[1_000, 10_000, 100_000].each { |n| bench_once(n) }
第一次執行僅作為暖機,第二次開始記錄;若希望更穩定,可將 3 次改為 10 次並計算中位數
數據(在 linux 跑,ruby 版本 2.7.8):
== size=1000
user system total real
plain_ruby 0.001151 0.000016 0.001167 ( 0.001108)
mongory_rb 0.005817 0.000000 0.005817 ( 0.005817)
== size=10000
user system total real
plain_ruby 0.008220 0.000015 0.008235 ( 0.008233)
mongory_rb 0.047256 0.000000 0.047256 ( 0.047257)
== size=100000
user system total real
plain_ruby 0.090509 0.004023 0.094532 ( 0.094532)
mongory_rb 0.477171 0.001838 0.479009 ( 0.479030)
不管在什麼情境下, Mongory-rb 都比純 Ruby filter 慢了約五倍!
而且這還是優化後的數據,優化前我記得差到 20 倍去了 :(
那時第一次把數據跑出來時,筆者其實有點失落:Mongory 明顯落後了純 Ruby 一整個數量級!
那段時間,筆者每天晚上反覆嘗試各種作法,後來把 matcher 的邏輯編譯成 Proc,讓整棵樹包成一個閉包,盡量降低 callstack 的深度;也做了調整遍歷順序等小優化
結果是有進步,但距離純 Ruby 仍然有差距
這不是否定結構化的價值,而是提醒筆者:若要達到「快且穩」,就必須往語言邊界下探,消除 Ruby 呼叫深度與分派開銷
因此,筆者決定啟動下一階段:以 C 語言實作核心(mongory-core),讓 matcher tree 的建構與匹配落在更低層的成本模型
但非本科出身的筆者從沒碰過 C,一上手就要搞這麼個小而精的項目,實在是個挑戰....
沒關係就從基礎開始,一步一步來
幹大事,我幹大的!
接下來 Day 9 將介紹測試先行與 Unity 的使用方式,解釋筆者如何把 C Core 切成小單元、逐步建構基礎設施與 matcher tree,並確保效能優化的同時不犧牲正確性