Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

เชี่ยวชาญ Memoization ใน Ruby: เพิ่มประสิทธิภาพด้วยเทคนิค Caching เรียบง่าย

วันนี้อยากพูดถึงเทคนิคโปรดสำหรับเพิ่มประสิทธิภาพ มันเป็นแหล่ง "ชัยชนะเล็กๆ" ที่สะสมเป็นผลใหญ่ และโอกาสทำลายแอปน้อยมาก เพียงนิดหน่อย

เทคนิคนี้เรียกว่า "Memoization" ถึงคำศัพท์วิทยาการคอมพิวเตอร์ราคา $10 แต่มันหมายถึง: แทนที่จะทำงานซ้ำทุกครั้งที่เรียกเมธอด ให้บันทึกค่าที่คืนไว้ในตัวแปร และใช้ตัวแปรนั้นแทน

รูปแบบ pseudocode:

def my_method
  @memo =  if @memo is undefined
  return @memo
end

วิธีทำใน Ruby นี่คือวิธีที่แข็งแกร่งที่สุด แต่ค่อนข้าง verbose มีวิธีสั้นกว่าที่ทำได้เกือบเหมือนกัน

class MyClass
  def my_method
    unless defined?(@my_method)
      @my_method = begin
        # ทำ calculation, database query
        # หรืองานที่ใช้เวลานานที่นี่
      end
    end
    @my_method
  end
end

โค้ดข้างบนทำ 3 สิ่ง:

  1. ตรวจสอบว่ามี instance variable ชื่อ @my_method หรือไม่
  2. หากยังไม่มี ทำงานและเก็บผลลัพธ์ไว้ใน @my_method
  3. คืนค่า @my_method

อย่าสับสนจากการมีทั้งเมธอดและ instance variable ชื่อ my_method ผมตั้งชื่อตัวแปรอะไรก็ได้ แต่ convention คือตั้งชื่อตามเมธอดที่ memoize

รุ่นย่อ (Shorthand)

ปัญหาของโค้ดข้างต้นคือ cumbersome ทำให้มักเห็นรุ่นย่อที่ทำเกือบเหมือนกัน:

class MyClass
  def my_method1
    @my_method1 ||= some_long_calculation
  end

  def my_method2
    @my_method2 ||= begin
      # begin-end block ให้ใช้หลายบรรทัดง่ายๆ
    end
  end
end

ทั้งคู่ใช้ตัวดำเนินการ a ||= b ของ Ruby ซึ่งเป็น shorthand สำหรับ a || (a = b) ซึ่งก็เป็น shorthand อีกทีสำหรับ:

# ไม่ควรใช้ return แบบนี้จริงๆ
# ใช้แค่แสดงความคิดให้มือใหม่เข้าใจว่า conditional evaluates ไปที่ค่าใน `a`
if a
  return a
else
  a = b
  return a
end

แหล่งมาของบั๊ก

หากคุณตั้งใจดู จะเห็นรุ่นย่อ evaluate "truthiness" ของตัวแปร memo แทนการตรวจสอบความมีอยู่ นี่คือข้อจำกัดหลัก: memoize nil หรือ false ไม่ได้

หลาย use case ไม่สำคัญ แต่เป็นข้อเท็จจริงที่น่ารำคาญต้องจดจำตอน memoize

Memoize เมธอดที่มีอาร์กิวเมนต์

จนถึงตอนนี้ memoize ค่าเดียว แต่ฟังก์ชันส่วนใหญ่ไม่คืนค่าเดิมเสมอ ลองดู Fibonacci sequence โจทย์สัมภาษณ์เทคนิคยอดนิยม

คำนวณ Fibonacci แบบ recursive ใน 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

ปัญหา: implementation นี้ไม่มีประสิทธิภาพ เพื่อพิสูจน์ ให้เพิ่ม 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 เดียวกันหลายครั้ง จำนวนการเรียกเพิ่มแบบ exponential กับ n

แก้ด้วย memoize ผลลัพธ์ของ 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

ตอน memoized แล้ว จำนวนการเรียกไม่เพิ่ม exponential อีกต่อไป

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

การทำให้หมดอายุ (Invalidation)

Memoization คล้าย caching แต่ผลลัพธ์ memoized ไม่หมดอายุอัตโนมัติ และมักไม่มีวิธีง่ายล้างค่าที่ตั้งไว้

สำหรับ Fibonacci ไม่สำคัญ เพราะ Fibonacci.calculate(10) คืนค่าเดิมตลอด แต่ use cases อื่นสำคัญ

เช่น โค้ดแบบนี้:

# ไม่ใช่ไอเดียดี
class User
  def full_name
    @full_name ||= [first_name, last_name].join(" ")
  end
end

ส่วนตัวผมไม่ memoize ที่นี่ เพราะถ้า first/last name เปลี่ยน full name อาจไม่ update

ที่สามารถคล่องตัวได้คือภายใน Rails controllers มักเห็นโค้ดแบบนี้:

class ApplicationController
  def current_user
    @current_user ||= User.find(...)
  end
end

นี้ OK เพราะ controller instance ถูก destroy หลังแต่ละ web request ไม่น่าจะมีการเปลี่ยน user ระหว่าง request ปกติ

กรณี streaming connections เช่น ActionCable อาจต้องระวังกว่า ผมไม่รู้ ไม่เคยใช้

การใช้มากเกินไป (Overuse)

สุดท้าย ต้องเตือน: อะไรก็ตาม overuse ได้ Memoization ควรใช้กับ expensive operations ที่จะไม่เปลี่ยนตลอดอายุของตัวแปร memo เท่านั้น