एप्लिकेशन विकसित करते समय हमारे पास अक्सर ऐसे तरीके होते हैं जो धीरे-धीरे चलते हैं। शायद उन्हें डेटाबेस से पूछताछ करने या बाहरी सेवा को हिट करने की आवश्यकता है, जो दोनों उन्हें धीमा कर सकते हैं। हम हर बार उस डेटा की आवश्यकता होने पर विधि को कॉल कर सकते हैं और केवल ओवरहेड स्वीकार कर सकते हैं, लेकिन यदि प्रदर्शन चिंता का विषय है तो हमारे पास कुछ विकल्प हैं।
एक के लिए, हम डेटा को एक चर के लिए असाइन कर सकते हैं और इसका पुन:उपयोग कर सकते हैं, जो प्रक्रिया को गति देगा। एक संभावित समाधान के रूप में, उस चर को मैन्युअल रूप से प्रबंधित करना जल्दी से थकाऊ हो सकता है।
लेकिन, क्या होगा यदि इसके बजाय, यह "धीमा काम" करने वाला तरीका हमारे लिए उस चर को संभाल सकता है? यह हमें उसी तरह से विधि को कॉल करने की अनुमति देगा, लेकिन विधि को डेटा को सहेजना और पुन:उपयोग करना होगा। संस्मरण ठीक यही करता है।
सीधे शब्दों में कहें, ज्ञापन एक विधि के वापसी मूल्य को सहेज रहा है, इसलिए इसे हर बार पुन:गणना करने की आवश्यकता नहीं है। सभी कैशिंग के साथ, आप प्रभावी रूप से समय के लिए मेमोरी का व्यापार कर रहे हैं (यानी आप मूल्य को स्टोर करने के लिए आवश्यक मेमोरी छोड़ देते हैं, लेकिन आप विधि को संसाधित करने के लिए आवश्यक समय बचाते हैं)।
किसी मान को कैसे याद रखें
रूबी या-बराबर ऑपरेटर के साथ मूल्यों को याद रखने के लिए एक बहुत ही साफ मुहावरा प्रदान करता है:||=
. यह एक तार्किक OR (||
. का उपयोग करता है ) बाएँ और दाएँ मानों के बीच, फिर परिणाम को बाईं ओर चर को निर्दिष्ट करता है। कार्रवाई में:
value ||= expensive_method(123)
#logically equivalent to:
value = (value || expensive_method(123))
संस्मरण कैसे काम करता है
यह समझने के लिए कि यह कैसे काम करता है, आपको दो अवधारणाओं को समझने की आवश्यकता है:"झूठे" मूल्य और आलसी मूल्यांकन। हम पहले सत्य-असत्य से शुरुआत करेंगे।
सत्य और असत्य
रूबी (लगभग सभी अन्य भाषाओं की तरह) में बूलियन true
. के लिए बिल्ट-इन कीवर्ड हैं और false
मूल्य। वे ठीक वैसे ही काम करते हैं जैसे आप उम्मीद करते हैं:
if true
#we always run this
end
if false
# this will never run
end
हालांकि, रूबी (और कई अन्य भाषाओं) में "सत्य" और "झूठे" मूल्यों की अवधारणा भी है। इसका मतलब है कि मानों को "जैसे" माना जा सकता है कि वे 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
आलसी मूल्यांकन
आलसी मूल्यांकन अनुकूलन का एक रूप है जो प्रोग्रामिंग भाषाओं में बहुत आम है। यह प्रोग्राम को उन कार्यों को छोड़ने की अनुमति देता है जो आवश्यक नहीं हैं।
तार्किक या ऑपरेटर (||
) यदि बाएँ या दाएँ पक्ष सत्य हैं, तो सत्य लौटाता है। इसका मतलब यह है कि यदि बाएँ हाथ का तर्क सत्य है तो दाएँ पक्ष का मूल्यांकन करने का कोई मतलब नहीं है क्योंकि हम पहले से ही जानते हैं कि परिणाम सत्य होगा। यदि हम इसे स्वयं लागू करते हैं तो हम कुछ इस तरह से समाप्त हो सकते हैं:
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
. के साथ जारी रखते हैं अछूता।
निर्णय लेना कि संस्मरण का उपयोग कब करना है
संस्मरण का उपयोग करते समय कुछ प्रश्न होते हैं जो हमें स्वयं से पूछने की आवश्यकता होती है:मूल्य का उपयोग कितनी बार किया जाता है? इसके बदलने का क्या कारण है? यह कितनी बार बदलता है?
अगर वैल्यू को केवल एक बार एक्सेस किया जाता है तो वैल्यू को कैशिंग करना बहुत उपयोगी नहीं होगा, जितनी बार वैल्यू को एक्सेस किया जाता है, उतना ही अधिक लाभ हम इसे कैशिंग से प्राप्त कर सकते हैं।
जब यह आता है कि यह किस कारण से बदलता है, तो हमें यह देखने की जरूरत है कि विधि में किन मूल्यों का उपयोग किया जाता है। क्या यह तर्क लेता है? यदि ऐसा है तो ज्ञापन को शायद इसे ध्यान में रखना होगा। व्यक्तिगत रूप से, मुझे इसके लिए मेमोइस्ट रत्न का उपयोग करना पसंद है क्योंकि यह आपके लिए तर्कों को संभालता है।
अंत में, हमें यह विचार करने की आवश्यकता है कि मूल्य कितनी बार बदलता है। क्या ऐसे उदाहरण चर हैं जो इसे बदलने का कारण बनते हैं? जब वे बदलते हैं तो क्या हमें कैश्ड मान को साफ़ करने की आवश्यकता होती है? क्या वैल्यू को ऑब्जेक्ट लेवल या क्लास लेवल पर कैश किया जाना चाहिए?
इन सवालों का जवाब देने के लिए आइए एक सरल उदाहरण देखें और निर्णयों के माध्यम से कदम उठाएं:
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
के लिए हालांकि, हमें पारित होने वाले प्रत्येक तर्क के लिए एक अलग मूल्य की आवश्यकता है। हम इसके लिए मेमोइस्ट का उपयोग कर सकते हैं, लेकिन स्पष्टता के लिए हम यहां अपना समाधान पेश करेंगे:
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
हो सकता है कि आप अक्सर कक्षा-स्तरीय संस्मरण नहीं चाहते हों, लेकिन यह एक विकल्प के रूप में है। हालांकि, अगर आपको इस स्तर पर कैश करने के लिए एक मूल्य की आवश्यकता है, तो शायद यह रेडिस या मेमकैच्ड जैसे बाहरी स्टोर के साथ मूल्य को कैशिंग करने के लायक है।
Ruby on Rails Applications में सामान्य संस्मरण उपयोग के मामले
रेल अनुप्रयोगों में, सबसे आम उपयोग-मामला जो मैं ज्ञापन के लिए देखता हूं वह डेटाबेस कॉल को कम कर रहा है, खासकर जब एक अनुरोध के भीतर कोई मान नहीं बदला जा रहा है। नियंत्रकों में रिकॉर्ड देखने के लिए "फाइंडर" विधियां इस तरह के डेटाबेस कॉल का एक अच्छा उदाहरण हैं जैसे:
def current_user
@current_user ||= User.find(params[:user_id])
end
एक अन्य सामान्य स्थान यह है कि यदि आप विचारों को प्रस्तुत करने के लिए किसी प्रकार के डेकोरेटर/प्रस्तुतकर्ता/व्यू-मॉडल प्रकार के आर्किटेक्चर का उपयोग करते हैं। इन वस्तुओं में विधियों में अक्सर संस्मरण के लिए अच्छे उम्मीदवार होते हैं क्योंकि वे केवल अनुरोध के जीवन के लिए बने रहते हैं, डेटा सामान्य रूप से उत्परिवर्तित नहीं होता है, और विचारों को प्रस्तुत करते समय कुछ विधियों को कई बार हिट किया जाता है।
संस्मरण गोचास
जब वास्तव में इसकी आवश्यकता नहीं होती है, तो सबसे बड़ी गठजोड़ में से एक चीजों को याद रखना है। स्ट्रिंग इंटरपोलेशन जैसी चीजें संस्मरण के लिए आसान उम्मीदवारों की तरह लग सकती हैं, लेकिन वास्तव में, वे आपकी साइट के प्रदर्शन पर कोई ध्यान देने योग्य प्रभाव पैदा करने की संभावना नहीं रखते हैं (जब तक कि आप असाधारण रूप से बड़े स्ट्रिंग्स का उपयोग नहीं कर रहे हैं या बहुत बड़ी मात्रा में स्ट्रिंग हेरफेर कर रहे हैं), उदाहरण के लिए:
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
आखिरी गोचा इस बात के कारण है कि आलसी मूल्यांकन कैसे काम करता है - यदि आपको ||=
के रूप में एक गलत मान (यानी शून्य या गलत) को याद करने की आवश्यकता है, तो आपको कुछ और कस्टम करना होगा। यदि आपका सहेजा गया मान गलत है, तो मुहावरा हमेशा दाईं ओर निष्पादित होगा। मेरे अनुभव में, आपको अक्सर इन मानों को कैश करने की आवश्यकता नहीं होती है, लेकिन यदि आप ऐसा करते हैं, तो आपको यह इंगित करने के लिए एक बूलियन ध्वज जोड़ने की आवश्यकता हो सकती है कि यह पहले से ही गणना की गई है, या किसी अन्य कैशिंग तंत्र का उपयोग करें।
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
जब मेमोइज़ेशन पर्याप्त नहीं है
आपके एप्लिकेशन के कुछ हिस्सों में प्रदर्शन को बेहतर बनाने के लिए संस्मरण एक सस्ता और प्रभावी तरीका हो सकता है, लेकिन यह इसकी कमियों के बिना नहीं है। एक बड़ी दृढ़ता है; सामान्य उदाहरण-स्तर के संस्मरण के लिए, मान केवल उस एक विशेष वस्तु के लिए सहेजा जाता है। यह वेब अनुरोध के जीवन के लिए मूल्यों को बचाने के लिए संस्मरण को बहुत अच्छा बनाता है, लेकिन आपको कैशिंग का पूरा लाभ नहीं देता है यदि आपके पास ऐसे मान हैं जो कई अनुरोधों के लिए समान होंगे और हर बार फिर से गणना की जा रही है।
कक्षा-स्तरीय संस्मरण इसमें मदद कर सकता है, लेकिन कैश अमान्यकरण को प्रबंधित करना अधिक कठिन हो जाता है। यह उल्लेख करने के लिए नहीं है कि यदि आपका सर्वर रीबूट करता है तो कैश किए गए मान खो जाएंगे, और उन्हें एकाधिक वेब सर्वरों के बीच साझा नहीं किया जा सकता है।
कैशिंग पर इस श्रृंखला के अगले अंक में हम इन समस्याओं के लिए रेल के समाधान को देखेंगे - निम्न-स्तरीय कैशिंग। आपको बाहरी स्टोर में मानों को कैश करने की अनुमति देता है जिसे सर्वरों के बीच साझा किया जा सकता है, और समाप्ति समयबाह्य और गतिशील कैश कुंजियों के साथ कैश अमान्यता को प्रबंधित करें।