วันนี้ฉันอยากจะพูดเกี่ยวกับหนึ่งในเทคนิคที่ฉันโปรดปรานในการปรับปรุงประสิทธิภาพ เป็นแหล่งที่มาของประสิทธิภาพเล็กๆ น้อยๆ ที่เพิ่มพูนขึ้นในที่สุด และลดการใช้งานของคุณลงเหลือเพียงเศษหินที่คุกรุ่นเป็นบางครั้งเท่านั้น เป็นครั้งคราวเท่านั้น
เทคนิคนี้เรียกว่า "การท่องจำ" แม้จะมีคำศัพท์วิทยาศาสตร์คอมพิวเตอร์มูลค่า 10 เหรียญ แต่ก็หมายความว่าแทนที่จะทำงานแบบเดิมทุกครั้งที่คุณเรียกใช้เมธอด คุณจะบันทึกค่าที่ส่งคืนไปยังตัวแปรและใช้ค่านั้นแทน
โค้ดเทียมมีลักษณะดังนี้:
def my_method
@memo = <work> if @memo is undefined
return @memo
end
และนี่คือวิธีที่คุณทำใน Ruby นี่เป็นแนวทางที่แข็งแกร่งที่สุด แต่ค่อนข้างละเอียด มีแนวทางอื่นๆ ที่กระชับกว่านี้ที่เราจะพูดคุยกันในภายหลัง
class MyClass
def my_method
unless defined?(@my_method)
@my_method = begin
# Do your calculation, database query
# or other long-running thing here.
end
end
@my_method
end
end
รหัสด้านบนทำสามสิ่ง:
- ตรวจสอบเพื่อดูว่ามีตัวแปรอินสแตนซ์ชื่อ
@my_method
. หรือไม่ . - ถ้ามี จะใช้งานได้และบันทึกผลลัพธ์ใน
@my_method
. - ส่งกลับ
@my_method
อย่าสับสนว่าเรามีทั้งเมธอดและตัวแปรอินสแตนซ์ชื่อ my_method
. ฉันสามารถตั้งชื่อตัวแปรของฉันอะไรก็ได้ แต่มันเป็นเรื่องธรรมดาที่จะตั้งชื่อตามเมธอดที่กำลังบันทึก
เวอร์ชันชวเลข
ปัญหาอย่างหนึ่งของโค้ดด้านบนคือค่อนข้างยุ่งยาก ด้วยเหตุนี้ คุณจึงมีแนวโน้มที่จะเห็นเวอร์ชันชวเลขซึ่งเกือบจะเหมือนกัน:
class MyClass
def my_method1
@my_method1 ||= some_long_calculation
end
def my_method2
@my_method2 ||= begin
# The begin-end block lets you easily
# use multiple lines of code here.
end
end
end
ทั้งสองใช้ a ||= b
. ของ Ruby โอเปอเรเตอร์ ซึ่งเป็นชวเลขสำหรับ a || (a = b)
ซึ่งเป็นตัวย่อมากหรือน้อยสำหรับ:
# You wouldn't use return like this in real life.
# I'm just using it to express to beginners the idea
# that the conditional evaluates to whatever winds up in `a`.
if a
return a
else
a = b
return a
end
ที่มาของบั๊ก
หากคุณให้ความสนใจอย่างใกล้ชิด คุณอาจสังเกตเห็นว่าเวอร์ชันชวเลขประเมิน "ความจริง" ของตัวแปรบันทึกแทนที่จะตรวจสอบการมีอยู่ของตัวแปรนั้น นี่คือที่มาของหนึ่งในข้อจำกัดที่สำคัญของเวอร์ชันชวเลข:มันไม่เคยจำ nil
หรือ false
.
มีกรณีการใช้งานมากมายที่สิ่งนี้ไม่สำคัญ แต่มันเป็นหนึ่งในข้อเท็จจริงที่น่ารำคาญที่คุณต้องจำไว้ทุกครั้งที่คุณท่องจำ
วิธีการบันทึกด้วยอาร์กิวเมนต์
จนถึงตอนนี้ เราได้ดำเนินการเฉพาะการท่องจำค่าเดียว แต่มีฟังก์ชันไม่มากที่ส่งคืนผลลัพธ์เดียวกันตลอดเวลา มาดูบทสัมภาษณ์ทางเทคนิคที่โปรดปรานกัน:ลำดับฟีโบนักชี
คุณสามารถคำนวณลำดับฟีโบนักชีแบบวนซ้ำใน Ruby ได้ดังนี้:
class Fibonacci
def self.calculate(n)
return n if n == 0 || n == 1
calculate(n - 1) + calculate(n - 2)
end
end
Fibonacci.calculate(10) # => 55
ปัญหาของการดำเนินการนี้คือไม่มีประสิทธิภาพ เพื่อพิสูจน์สิ่งนี้ ให้เพิ่ม print
คำสั่งเพื่อดูค่าของ n
.
class Fibonacci
def self.calculate(n)
print "#{ n } "
return n if n == 0 || n == 1
calculate(n - 1) + calculate(n - 2)
end
end
Fibonacci.calculate(4)
# Outputs: 4 3 2 1 0 1 2 1 0
อย่างที่คุณเห็น calculate
ถูกเรียกซ้ำๆ ด้วยค่า n
. ที่เหมือนกันหลายค่า . อันที่จริงจำนวนการโทรไปที่ calculate
จะเติบโตแบบทวีคูณด้วย n
.
วิธีหนึ่งในการแก้ปัญหานี้คือการจดจำผลลัพธ์ของ calculate
. การทำเช่นนี้ไม่ได้แตกต่างจากตัวอย่างการท่องจำอื่นๆ ที่เรากล่าวถึงมากนัก
class Fibonacci
def self.calculate(n)
@calculate ||= {}
@calculate[n] ||= begin
print "#{ n } "
if n == 0 || n == 1
n
else
calculate(n - 1) + calculate(n - 2)
end
end
end
end
Fibonacci.calculate(4)
# Outputs: 4 3 2 1 0
ตอนนี้เราได้บันทึก calculate
จำนวนการโทรไม่เพิ่มขึ้นแบบทวีคูณด้วย n
. อีกต่อไป .
Fibonacci.calculate(20)
# Outputs: 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
ใช้ไม่ได้
การท่องจำนั้นคล้ายกับการแคชมาก ยกเว้นว่าผลลัพธ์ที่บันทึกไว้จะไม่หมดอายุโดยอัตโนมัติ และโดยปกติแล้วจะไม่มีวิธีง่ายๆ ในการล้างข้อมูลเหล่านี้เมื่อตั้งค่าไว้
สำหรับกรณีการใช้งานเช่นตัวสร้างลำดับฟีโบนักชี เรื่องนี้แทบจะไม่มีความสำคัญ Fibonacci.calculate(10)
จะให้ผลลัพธ์เหมือนเดิมเสมอ แต่ในกรณีการใช้งานอื่น ๆ มันไม่สำคัญ
ตัวอย่างเช่น คุณอาจเห็นโค้ดดังนี้:
# Not the best idea
class User
def full_name
@full_name ||= [first_name, last_name].join(" ")
end
end
โดยส่วนตัวแล้วฉันจะไม่ใช้การท่องจำที่นี่ เพราะหากชื่อหรือนามสกุลมีการเปลี่ยนแปลง ชื่อเต็มอาจไม่ได้รับการอัปเดต
ที่เดียวที่คุณสามารถผ่อนคลายได้อีกนิดคือภายในตัวควบคุม Rails เป็นเรื่องปกติมากที่จะเห็นโค้ดแบบนี้:
class ApplicationController
def current_user
@current_user ||= User.find(...)
end
end
ไม่เป็นไร เพราะอินสแตนซ์ของตัวควบคุมจะถูกทำลายหลังจากคำขอเว็บแต่ละครั้ง ไม่น่าเป็นไปได้ที่ผู้ใช้ที่เข้าสู่ระบบในปัจจุบันจะเปลี่ยนแปลงในระหว่างการขอตามปกติใดๆ
เมื่อต้องรับมือกับการเชื่อมต่อสตรีมมิ่งเช่น ActionCable คุณอาจต้องระวังให้มากกว่านี้ ฉันไม่รู้. ฉันไม่เคยใช้มัน
ใช้มากเกินไป
สุดท้ายนี้ ฉันรู้สึกว่าควรที่จะชี้ให้เห็นว่า เป็นไปได้ไหมที่จะท่องจำมากเกินไป เป็นเทคนิคที่ควรนำไปใช้กับการดำเนินการที่มีราคาแพงเท่านั้น ซึ่งจะไม่มีวันเปลี่ยนแปลงตลอดอายุของตัวแปรบันทึกช่วยจำ