เมื่อพัฒนาแอปพลิเคชัน เรามักมีวิธีการที่ทำงานช้า บางทีพวกเขาอาจต้องการสอบถามฐานข้อมูล หรือกดบริการภายนอก ซึ่งทั้งสองอย่างนี้อาจทำให้ช้าลงได้ เราสามารถเรียกใช้เมธอดทุกครั้งที่ต้องการข้อมูลนั้นและยอมรับโอเวอร์เฮด แต่ถ้าประสิทธิภาพเป็นปัญหา เราก็มีตัวเลือกบางอย่าง
ประการหนึ่ง เราสามารถกำหนดข้อมูลให้กับตัวแปรและนำกลับมาใช้ใหม่ได้ ซึ่งจะทำให้กระบวนการเร็วขึ้น ในขณะที่วิธีแก้ปัญหาที่เป็นไปได้ การจัดการตัวแปรนั้นด้วยตนเองอาจกลายเป็นเรื่องน่าเบื่อได้อย่างรวดเร็ว
แต่ถ้าวิธีการทำ "งานช้า" นี้สามารถจัดการกับตัวแปรนั้นแทนเราได้ ซึ่งจะทำให้เราสามารถเรียกใช้เมธอดในลักษณะเดียวกันได้ แต่มีวิธีการบันทึกและนำข้อมูลกลับมาใช้ใหม่ นี่คือสิ่งที่การท่องจำทำ
พูดง่ายๆ ก็คือ การช่วยจำเป็นการบันทึกค่าที่ส่งกลับของเมธอด ดังนั้นจึงไม่จำเป็นต้องคำนวณใหม่ทุกครั้ง เช่นเดียวกับการแคชทั้งหมด คุณกำลังแลกเปลี่ยนหน่วยความจำอย่างมีประสิทธิภาพสำหรับเวลา (เช่น คุณเลิกใช้หน่วยความจำที่จำเป็นในการจัดเก็บค่า แต่คุณประหยัดเวลาที่จำเป็นในการประมวลผลวิธีการ)
วิธีการบันทึกมูลค่า
Ruby ให้สำนวนที่ชัดเจนสำหรับการจดจำค่าด้วยตัวดำเนินการ or-equals:||=
. สิ่งนี้ใช้ตรรกะ OR (||
) ระหว่างค่าซ้ายและขวา จากนั้นกำหนดผลลัพธ์ให้กับตัวแปรทางด้านซ้าย ในการดำเนินการ:
value ||= expensive_method(123)
#logically equivalent to:
value = (value || expensive_method(123))
การจดบันทึกทำงานอย่างไร
เพื่อให้เข้าใจวิธีการทำงาน คุณต้องเข้าใจแนวคิดสองประการ:ค่า "เท็จ" และการประเมินแบบขี้เกียจ เราจะเริ่มต้นด้วยความจริง-เท็จก่อน
ความจริงและเท็จ
Ruby (เหมือนกับภาษาอื่นๆ เกือบทั้งหมด) มีคีย์เวิร์ดในตัวสำหรับบูลีน true
และ false
ค่า มันทำงานได้ตรงตามที่คุณคาดหวัง:
if true
#we always run this
end
if false
# this will never run
end
อย่างไรก็ตาม Ruby (และภาษาอื่นๆ อีกมากมาย) มีแนวคิดเกี่ยวกับค่านิยม "ความจริง" และ "เท็จ" ซึ่งหมายความว่าค่าต่างๆ ได้รับการปฏิบัติ "ราวกับว่า" เป็นค่า true
หรือ false
. ในทับทิม เท่านั้น nil
และ false
เป็นเท็จ ค่าอื่นๆ ทั้งหมด (รวมศูนย์) จะถือเป็น true
(หมายเหตุ:ภาษาอื่นสร้างทางเลือกที่แตกต่างกัน ตัวอย่างเช่น C ถือว่าศูนย์เป็น false
). ใช้ตัวอย่างของเราจากด้านบนซ้ำ เรายังสามารถเขียน:
value = "abc123" # a string
if value
# we always run this
end
value = nil
if value
# this will never run
end
ประเมินขี้เกียจ
การประเมินความขี้เกียจเป็นรูปแบบหนึ่งของการเพิ่มประสิทธิภาพที่ใช้บ่อยมากในภาษาโปรแกรม ทำให้โปรแกรมสามารถข้ามการทำงานที่ไม่จำเป็นได้
ตัวดำเนินการตรรกะ OR (||
) คืนค่า จริง หากด้านซ้ายหรือด้านขวาเป็น จริง ซึ่งหมายความว่าหากอาร์กิวเมนต์มือซ้ายเป็นจริง ก็ไม่มีประโยชน์ที่จะประเมินทางขวามือ เนื่องจากเราทราบแล้วว่าผลลัพธ์จะเป็นจริง หากเราดำเนินการด้วยตนเอง เราอาจจบลงด้วยสิ่งนี้:
def logical_or (lhs, rhs)
return lhs if lhs
rhs
end
ถ้า lhs
และ rhs
เป็นฟังก์ชัน (เช่น lamdas) คุณจะเห็น rhs
จะดำเนินการก็ต่อเมื่อ lhs
เป็นเท็จ
หรือเท่ากับ
การรวมสองแนวคิดของค่าความจริง-เท็จและการประเมินที่ขี้เกียจแสดงให้เราเห็นว่า ||=
โอเปอเรเตอร์กำลังทำ:
value #defaults to nil
value ||= "test"
value ||= "blah"
puts value
=> test
เราเริ่มต้นด้วยค่าที่ nil
เพราะมันไม่ได้เริ่มต้น ต่อไปเราจะพบกับ ||=
. ตัวแรกของเรา โอเปอเรเตอร์ value
เป็นเท็จในขั้นตอนนี้ดังนั้นเราจึงประเมินทางด้านขวา ("test"
) และกำหนดผลลัพธ์ให้กับ value
.ตอนนี้เราตีสอง ||=
โอเปอเรเตอร์ แต่คราวนี้ value
เป็นความจริงเพราะมันมีค่า "test"
. เราข้ามการประเมินทางด้านขวาและดำเนินการต่อด้วย value
ไม่ถูกแตะต้อง
ตัดสินใจว่าจะใช้บันทึกเมื่อใด
เมื่อใช้การท่องจำ มีคำถามบางอย่างที่เราต้องถามตัวเอง:ค่าเข้าถึงบ่อยแค่ไหน? อะไรทำให้เกิดการเปลี่ยนแปลง? เปลี่ยนแปลงบ่อยแค่ไหน
หากเข้าถึงค่าได้เพียงครั้งเดียว จากนั้นการแคชค่าจะไม่มีประโยชน์มาก ยิ่งเข้าถึงค่าบ่อยเท่าใด เราก็จะได้รับประโยชน์จากการแคชมากขึ้นเท่านั้น
เมื่อพูดถึงสาเหตุที่ทำให้เกิดการเปลี่ยนแปลง เราต้องดูว่าค่าใดที่ใช้ในวิธีการนี้ มันใช้อาร์กิวเมนต์หรือไม่? ถ้าเป็นเช่นนั้นการท่องจำอาจต้องคำนึงถึงสิ่งนี้ด้วย โดยส่วนตัวแล้วฉันชอบใช้ memoist gem เพราะมันจัดการกับข้อโต้แย้งสำหรับคุณ
สุดท้ายนี้ เราต้องพิจารณาว่าค่านิยมเปลี่ยนแปลงบ่อยแค่ไหน มีตัวแปรอินสแตนซ์ที่ทำให้เปลี่ยนแปลงหรือไม่ เราจำเป็นต้องล้างค่าแคชเมื่อมีการเปลี่ยนแปลงหรือไม่ ค่าควรถูกแคชไว้ที่ระดับอ็อบเจ็กต์หรือระดับคลาสหรือไม่
ในการตอบคำถามเหล่านี้ มาดูตัวอย่างง่ายๆ และทำตามขั้นตอนการตัดสินใจ:
class ProfitLossReport
def initialize(title, expenses, invoices)
@expenses = expenses
@invoices = invoices
@title = title
end
def title
"#{@title} #{Time.current}"
end
def cost
@expenses.sum(:amount)
end
def revenue
@invoices.sum(:amount)
end
def profit
revenue - cost
end
def average_profit(months)
profit / months.to_f
end
end
ไม่แสดงรหัสโทรศัพท์ที่นี่ แต่เป็นการเดาที่ดีว่า title
เมธอดน่าจะเรียกเพียงครั้งเดียว แต่ยังใช้ Time.current
ดังนั้นการท่องจำอาจหมายถึงค่าที่จะค้างในทันที
revenue
และ cost
เมธอดถูกโจมตีหลายครั้งแม้จะอยู่ในคลาสนี้ เนื่องจากทั้งคู่ต้องการการตีฐานข้อมูล พวกเขาจึงเป็นตัวเลือกหลักสำหรับการจดจำหากประสิทธิภาพกลายเป็นปัญหา สมมติว่าเราบันทึกสิ่งเหล่านี้ แล้ว profit
ไม่จำเป็นต้องจดบันทึก มิฉะนั้น เราแค่เพิ่มแคชที่ด้านบนของแคชเพื่อให้ได้กำไรน้อยที่สุด
สุดท้าย เรามี average_profit
. ค่าในที่นี้ขึ้นอยู่กับการโต้แย้ง ดังนั้นการท่องจำของเราจึงต้องคำนึงถึงสิ่งนี้ด้วย สำหรับกรณีง่ายๆ เช่น revenue
เราทำได้แค่นี้:
def revenue
@revenue ||= @invoices.sum(:amount)
end
สำหรับ average_profit
แม้ว่าเราต้องการค่าที่แตกต่างกันสำหรับแต่ละอาร์กิวเมนต์ที่ถูกส่งผ่าน เราสามารถใช้ memoist สำหรับสิ่งนี้ แต่เพื่อความชัดเจน เราจะนำเสนอวิธีแก้ปัญหาของเราที่นี่:
def average_profit(months)
@average_profit ||= {}
@average_profit[months] ||= profit / months.to_f
end
ที่นี่เราใช้แฮชเพื่อติดตามค่าที่คำนวณได้ของเรา อันดับแรก เราต้องแน่ใจว่า @average_profit
ได้รับการเริ่มต้นแล้วเราใช้อาร์กิวเมนต์ที่ส่งผ่านเข้ามาเป็นคีย์แฮช
การจดจำที่ระดับชั้นเรียนหรือระดับอินสแตนซ์
การบันทึกเวลาส่วนใหญ่ทำในระดับอินสแตนซ์ หมายความว่าเราใช้ตัวแปรอินสแตนซ์เพื่อเก็บค่าที่คำนวณไว้ นอกจากนี้ยังหมายความว่าเมื่อใดก็ตามที่เราสร้างอินสแตนซ์ใหม่ของวัตถุจะไม่ได้รับประโยชน์จากค่า "แคช" นี่เป็นภาพประกอบที่เรียบง่ายมาก:
class MemoizedDemo
def value
@value ||= computed_value
end
def computed_value
puts "Crunching Numbers"
rand(100)
end
end
โดยใช้วัตถุนี้ เราสามารถเห็นผล:
demo = MemoizedDemo.new
=> #<MemoizedDemo:0x00007f95e5d9d398>
demo.value
Crunching Numbers
=> 19
demo.value
=> 19
MemoizedDemo.new.value
Crunching Numbers
=> 93
เราสามารถเปลี่ยนสิ่งนี้ได้โดยใช้ตัวแปรระดับคลาส (ด้วย @@
) สำหรับค่าที่เราบันทึกไว้:
def value
@@value ||= computed_value
end
ผลลัพธ์จะกลายเป็น:
demo = MemoizedDemo.new
=> #<MemoizedDemo:0x00007f95e5d9d398>
demo.value
Crunching Numbers
=> 60
demo.value
=> 60
MemoizedDemo.new.value
=> 60
คุณอาจไม่ต้องการการท่องจำระดับชั้นเรียนบ่อยๆ แต่มีตัวเลือกให้ อย่างไรก็ตาม หากคุณต้องการแคชค่าที่ระดับนี้ คุณควรดูที่การแคชค่ากับร้านค้าภายนอก เช่น Redis หรือ memcached
การใช้งานบันทึกทั่วไปในแอปพลิเคชัน Ruby on Rails
ในแอปพลิเคชัน Rails กรณีการใช้งานทั่วไปที่ฉันเห็นสำหรับการบันทึกคือลดการเรียกฐานข้อมูล โดยเฉพาะอย่างยิ่งเมื่อค่าจะไม่เปลี่ยนแปลงภายในคำขอเดียว วิธีการ "Finder" สำหรับค้นหาระเบียนในตัวควบคุมเป็นตัวอย่างที่ดีของการเรียกฐานข้อมูลประเภทนี้ เช่น:
def current_user
@current_user ||= User.find(params[:user_id])
end
สถานที่ทั่วไปอีกประการหนึ่งคือถ้าคุณใช้สถาปัตยกรรมประเภทมัณฑนากร/พรีเซ็นเตอร์/ดูโมเดลบางประเภทสำหรับการแสดงผลมุมมอง เมธอดในออบเจ็กต์เหล่านี้มักมีตัวเลือกที่ดีสำหรับการจดบันทึก เนื่องจากจะคงอยู่ตลอดอายุของคำขอ โดยปกติแล้วข้อมูลจะไม่กลายพันธุ์ และวิธีการบางอย่างอาจถูกโจมตีหลายครั้งเมื่อแสดงมุมมอง
จดบันทึก Gotchas
หนึ่งใน gotchas ที่ใหญ่ที่สุดคือการจดจำสิ่งต่าง ๆ เมื่อไม่จำเป็นจริงๆ สิ่งต่างๆ เช่น การสอดแทรกสตริงอาจดูเหมือนง่ายสำหรับการจดบันทึก แต่ในความเป็นจริง สิ่งเหล่านี้ไม่น่าจะส่งผลกระทบที่เห็นได้ชัดเจนต่อประสิทธิภาพของไซต์ของคุณ (เว้นแต่ว่าคุณกำลังใช้สตริงที่มีขนาดใหญ่เป็นพิเศษหรือทำการดัดแปลงสตริงเป็นจำนวนมาก) ตัวอย่างเช่น:
def title
# memoization here is not going to have much of an impact on our performance
@title ||= "#{@object.published_at} - #{@object.title}"
end
อีกสิ่งหนึ่งที่ควรระวังคือการใช้แคชของเพื่อนเก่าของเราเป็นโมฆะ โดยเฉพาะอย่างยิ่งหากค่าที่บันทึกไว้ของคุณขึ้นอยู่กับสถานะของวัตถุ วิธีหนึ่งที่จะช่วยป้องกันสิ่งนี้คือการแคชที่ระดับต่ำสุดที่คุณสามารถทำได้ แทนที่จะแคชวิธีการคำนวณ a + b
มันอาจจะดีกว่าที่จะแคช a
และ b
ทีละวิธี
# Instead of this
def profit
# anyone else calling 'revenue' or 'losses' is not benefitting from the caching here
# and what happens if the 'revenue' or 'losses' value changes, will we remember to update profit?
@profit ||= (revenue - losses)
end
# try this
def profit
# no longer cached, but subtraction is a fast calculation
revenue - losses
end
def revenue
@revenue ||= Invoice.all.sum(:amount)
end
def losses
@losses ||= Purchase.all.sum(:amount)
end
gotcha ล่าสุดเกิดจากการทำงานของการประเมินแบบขี้เกียจ - คุณจะต้องทำบางอย่างที่กำหนดเองมากกว่านี้หากคุณต้องการบันทึกค่าเท็จ (เช่นศูนย์หรือเท็จ) เป็น ||=
สำนวนจะดำเนินการทางด้านขวามือเสมอหากค่าที่คุณบันทึกไว้เป็นเท็จ จากประสบการณ์ของผม คุณไม่จำเป็นต้องแคชค่าเหล่านี้บ่อยนัก แต่ถ้าเป็นเช่นนั้น คุณอาจต้องเพิ่มแฟล็กบูลีนเพื่อระบุว่ามีการคำนวณแล้ว หรือใช้กลไกการแคชอื่น
def last_post
# if the user has no posts, we will hit the database every time this method is called
@last_post ||= Post.where(user: current_user).order_by(created_at: :desc).first
end
# As a simple workaround we could do something like:
def last_post
return @last_post if @last_post_checked
@last_post_checked = true
@last_post ||= Post.where(user: current_user).order_by(created_at: :desc).first
end
เมื่อการท่องจำไม่เพียงพอ
การจดบันทึกอาจเป็นวิธีที่ประหยัดและมีประสิทธิภาพในการปรับปรุงประสิทธิภาพในส่วนของแอปพลิเคชันของคุณ แต่ก็ไม่ใช่ว่าจะไม่มีข้อเสีย สิ่งที่ยิ่งใหญ่อย่างหนึ่งคือความพากเพียร สำหรับการท่องจำระดับอินสแตนซ์ทั่วไป ค่าจะถูกบันทึกสำหรับออบเจกต์เฉพาะนั้นเท่านั้น วิธีนี้ทำให้การจดบันทึกเหมาะอย่างยิ่งสำหรับการบันทึกค่าตลอดอายุของคำขอทางเว็บ แต่จะไม่ให้ประโยชน์เต็มที่จากการแคชหากคุณมีค่าที่เหมือนกันสำหรับคำขอหลายรายการและถูกคำนวณใหม่ทุกครั้ง
การบันทึกระดับคลาสสามารถช่วยในเรื่องนี้ได้ แต่การจัดการการทำให้แคชใช้ไม่ได้นั้นยากขึ้น ไม่ต้องพูดถึงว่าหากเซิร์ฟเวอร์ของคุณรีบูตค่าแคชเหล่านั้นจะสูญหาย และจะไม่สามารถแชร์กับเว็บเซิร์ฟเวอร์หลายเครื่องได้
ในฉบับต่อไปของซีรีส์นี้เกี่ยวกับการแคช เราจะดูวิธีแก้ไขปัญหาเหล่านี้ของ Rails - การแคชระดับต่ำ ช่วยให้คุณแคชค่าไปยังร้านค้าภายนอกที่สามารถแชร์ระหว่างเซิร์ฟเวอร์ และจัดการการทำให้แคชใช้ไม่ได้ด้วยการหมดเวลาหมดอายุและคีย์แคชแบบไดนามิก