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

เร่งความเร็ว Rails ด้วย Memoization

เมื่อพัฒนาแอปพลิเคชัน เรามักมีวิธีการที่ทำงานช้า บางทีพวกเขาอาจต้องการสอบถามฐานข้อมูล หรือกดบริการภายนอก ซึ่งทั้งสองอย่างนี้อาจทำให้ช้าลงได้ เราสามารถเรียกใช้เมธอดทุกครั้งที่ต้องการข้อมูลนั้นและยอมรับโอเวอร์เฮด แต่ถ้าประสิทธิภาพเป็นปัญหา เราก็มีตัวเลือกบางอย่าง

ประการหนึ่ง เราสามารถกำหนดข้อมูลให้กับตัวแปรและนำกลับมาใช้ใหม่ได้ ซึ่งจะทำให้กระบวนการเร็วขึ้น ในขณะที่วิธีแก้ปัญหาที่เป็นไปได้ การจัดการตัวแปรนั้นด้วยตนเองอาจกลายเป็นเรื่องน่าเบื่อได้อย่างรวดเร็ว

แต่ถ้าวิธีการทำ "งานช้า" นี้สามารถจัดการกับตัวแปรนั้นแทนเราได้ ซึ่งจะทำให้เราสามารถเรียกใช้เมธอดในลักษณะเดียวกันได้ แต่มีวิธีการบันทึกและนำข้อมูลกลับมาใช้ใหม่ นี่คือสิ่งที่การท่องจำทำ

พูดง่ายๆ ก็คือ การช่วยจำเป็นการบันทึกค่าที่ส่งกลับของเมธอด ดังนั้นจึงไม่จำเป็นต้องคำนวณใหม่ทุกครั้ง เช่นเดียวกับการแคชทั้งหมด คุณกำลังแลกเปลี่ยนหน่วยความจำอย่างมีประสิทธิภาพสำหรับเวลา (เช่น คุณเลิกใช้หน่วยความจำที่จำเป็นในการจัดเก็บค่า แต่คุณประหยัดเวลาที่จำเป็นในการประมวลผลวิธีการ)

วิธีการบันทึกมูลค่า

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 - การแคชระดับต่ำ ช่วยให้คุณแคชค่าไปยังร้านค้าภายนอกที่สามารถแชร์ระหว่างเซิร์ฟเวอร์ และจัดการการทำให้แคชใช้ไม่ได้ด้วยการหมดเวลาหมดอายุและคีย์แคชแบบไดนามิก