Wednesday, November 16, 2005

Cache with Ruby Hash

เวลาเราสร้าง Hash object ใน Ruby
เราสามารถ pass block เข้าไปได้ด้วย
ซึ่ง block นี้จะถูกเรียกใช้
เมื่อมีการ access hash ตาม key ที่ให้มา แล้วไม่พบค่าใน hash นั้น
ลองดู code
require 'pp'
h = Hash.new { |h, key|
h[key] = "result from #{key}"
}
puts h[1]
puts h[2]
pp h

ได้ผลลัพท์ดังนี้
result from 1
result from 2
{1=>"result from 1", 2=>"result from 2"}


Feature นี้ทำให้เราสามารถ implement cache ได้อย่างง่ายๆ
ลองดูตัวอย่างนี้
def fib(n)
return n if n < 2
fib(n-1) + fib(n-2)
end
puts fib(20)

เขียนแบบใช้ Hash จะได้ดังนี้

fib_h = Hash.new {|h, k|
if k < 2
h[k] = k
else
h[k] = h[k-1] +h[k-2]
end
}
puts fib_h[30]


ทดลองจับเวลา เปรียบเทียบการหาค่า n ที่เท่ากับ 30
ถ้าไม่มี cache จะใช้เวลา 4.3 วินาที
ส่วน version ที่ใช้ cache จะใช้เวลา 0.002 วินาที

ทีนี้ลองดู version พิสดารบ้าง
ไปเห็นมาจาก Concise Memoization
กับของ Cheap Ruby memoize using Hash default block

version พิสดาร จะใช้แบบนี้
สมมติเรามี method fib แบบ simple
def fib(n)
return n if n < 2
fib(n-1) + fib(n-2)
end

กรณีที่เราต้องการทำ cache method นี้
เราก็จะสั่งดังนี้
memoize(:fib)

โดย method memoize มีหน้าตาดังนี้
def memoize( name )
meth = method( name )
cache = Hash.new { |cache, args|
cache[args] = meth.call( *args )
}
( class << self ; self ; end ).class_eval do
define_method( name ) { |*args| cache[args] }
end
cache
end

จะเห็นมีว่าการนำ class_eval กับ define_method มาใช้
คนที่ยังไม่คุ้นกับ 2 method นี้ลองดูตัวอย่างนี้
class X
def hi
puts 'Hello'
end
end

X.new.hi # output -> Hello

X.class_eval do
define_method(:hi) { |*args|
puts 'hi pok'
}
end

X.new.hi # output -> hi pok

Related link from Roti

3 comments:

bact' said...

อืม ถ้าเอาไปใช้กะพวก app ที่เรียก db บ่อย ๆ แบบนี้น่าจะช่วยเรื่องลด db query ได้ด้วย
คือไม่ต้องเริ่มสร้าง connection หรือส่ง query เลย
ถ้าเคยเรียกมาแล้ว ก็ส่งผลลัพธ์กลับได้เลย
(แต่ข้อมูลไม่ real-time ต้องดูประเภทข้อมูลด้วย)

ดีแฮะ

bact' said...

อ๊ะ .. ขึ้นอยู่กับขนาดหน่วยความจำด้วยนี่นา
space vs time
ส่ง code block เข้า Hash ใช้หน่วยความจำเท่าไหร่ ?
(แล้วทุก ๆ ครั้งที่เรียก code block นั้นด้วย parameter ที่ไม่เหมือนเดิม ก็ต้อง add code block ใหม่เข้า Hash ด้วย ? ... แล้วเมื่อไหร่เราจะรู้ว่า ควรจะเอา code block ไหนออกจาก Hash ได้แล้ว? .. ไม่งั้นมันก็โตเอา ๆ)

polawat phetra said...

ถ้าจะเอาไปใช้เป็น cache จริงๆ
ต้องเพิ่ม function ส่วนที่ทำหน้าที่
scan cache เพื่อ flush เอา cache item เก่าๆ ออกไปด้วย
เพื่อ limit จำนวน memory ที่ใช้

กรณีที่เอาไปใช้กับ web app
ไว้จะ post กรณีที่เคยใช้ ให้ฟัง(ดู)