एप्लिकेशन विकसित करते समय हमारे पास अक्सर ऐसे मेथड्स होते हैं जो धीरे चलते हैं। शायद उन्हें डेटाबेस क्वेरी करने की आवश्यकता होती है, या किसी बाहरी सेवा को हिट करना पड़ता है, दोनों ही उन्हें धीमा कर सकते हैं। हम हर बार जब उस डेटा की आवश्यकता होती है तो मेथड को कॉल कर सकते हैं और सिर्फ ओवरहेड स्वीकार कर सकते हैं, लेकिन अगर प्रदर्शन एक चिंता का विषय है तो हमारे पास कुछ विकल्प हैं।
एक के लिए, हम डेटा को एक वेरिएबल में असाइन कर सकते हैं और इसका पुनः उपयोग कर सकते हैं, जो प्रक्रिया को गति देगा। जबकि एक संभावित समाधान, मैन्युअल रूप से उस वेरिएबल को प्रबंधित करना जल्दी से थकाऊ हो सकता है।
लेकिन, क्या होगा अगर इसके बजाय, यह मेथड जो यह "धीमा काम" करता है, बस उस वेरिएबल को हमारे लिए संभाल सके? यह हमें उसी तरह मेथड को कॉल करने की अनुमति देगा, लेकिन मेथड डेटा को सेव और पुनः उपयोग करेगा। यही वास्तव में मेमोइज़ेशन करता है।
सीधे शब्दों में कहें तो, मेमोइज़ेशन एक मेथड के रिटर्न मान को सहेज रहा है ताकि इसकी हर बार पुनर्गणना न करनी पड़े। सभी कैशिंग की तरह, आप प्रभावी रूप से मेमोरी के लिए समय का व्यापार कर रहे हैं (यानी आप मान को संग्रहीत करने के लिए आवश्यक मेमोरी छोड़ देते हैं, लेकिन आप मेथड को प्रोसेस करने के लिए आवश्यक समय बचाते हैं)।
किसी मान को मेमोइज़ कैसे करें
रूबी या-इक्वल्स ऑपरेटर: ||= के साथ मानों को मेमोइज़ करने के लिए एक बहुत ही साफ मुहावरा प्रदान करता है। यह बाएं और दाएं मानों के बीच एक तार्किक OR (||) का उपयोग करता है, फिर परिणाम को बाएं वेरिएबल को असाइन करता है। एक्शन में:
value ||= expensive_method(123)
# तार्किक रूप से समकक्ष: value = (value || expensive_method(123))
मेमोइज़ेशन कैसे काम करता है
यह समझने के लिए कि यह कैसे काम करता है, आपको दो अवधारणाओं को समझने की आवश्यकता है: "फॉल्सी" मान और आलसी मूल्यांकन। हम ट्रूथी-फॉल्सी से शुरू करेंगे।
ट्रूथी और फॉल्सी
रूबी (लगभग सभी अन्य भाषाओं की तरह) बूलियन true और false मानों के लिए बिल्ट-इन कीवर्ड्स हैं। वे ठीक वैसे ही काम करते हैं जैसा आप उम्मीद करेंगे:
if true
# हम हमेशा इसे चलाते हैं
end
if false
# यह कभी नहीं चलेगा
end
हालांकि, रूबी (और कई अन्य भाषाओं) में "ट्रूथी" और "फॉल्सी" मानों की अवधारणा भी है। इसका मतलब है कि मानों को "ऐसे ही" माना जा सकता है जैसे वे true या false थे। रूबी में केवल nil और false फॉल्सी हैं। अन्य सभी मान (शून्य सहित) को true माना जाता है (नोट: अन्य भाषाएं अलग विकल्प चुनती हैं। उदाहरण के लिए C शून्य को false मानता है)। ऊपर से हमारे उदाहरण का पुनः उपयोग करते हुए, हम यह भी लिख सकते थे:
value = "abc123" # एक स्ट्रिंग
if value
# हम हमेशा इसे चलाते हैं
end
value = nil
if value
# यह कभी नहीं चलेगा
end
आलसी मूल्यांकन
आलसी मूल्यांकन एक प्रकार का अनुकूलन है जो प्रोग्रामिंग भाषाओं में बहुत आम है। यह प्रोग्राम को उन ऑपरेशन्स को छोड़ने की अनुमति देता है जो आवश्यक नहीं हैं।
तार्किक OR ऑपरेटर (||) सच लौटाता है यदि बाएं या दाएं हाथ की ओर कोई भी सच है। इसका मतलब है कि यदि बाएं हाथ का तर्क सच है तो दाएं हाथ की ओर का मूल्यांकन करने का कोई मतलब नहीं है क्योंकि हम पहले से ही जानते हैं कि परिणाम सच होगा। यदि हम इसे स्वयं लागू करते तो हम कुछ इस तरह समाप्त हो सकते थे:
def logical_or (lhs, rhs)
return lhs if lhs
rhs
end
यदि lhs और rhs फंक्शन थे (उदाहरण के लिए लैम्ब्डा) तो आप देख सकते हैं कि rhs केवल तभी निष्पादित होगा जब lhs फॉल्सी हो।
या-इक्वल्स
ट्रूथी-फॉल्सी मानों और आलसी मूल्यांकन की इन दो अवधारणाओं को मिलाकर हमें दिखाता है कि ||= ऑपरेटर क्या कर रहा है:
value # डिफ़ॉल्ट रूप से nil क्योंकि इसे इनिशियलाइज़ नहीं किया गया था
value ||= "test"
value ||= "blah"
puts value => test
हम value के nil होने से शुरू करते हैं क्योंकि इसे इनिशियलाइज़ नहीं किया गया था। अगला, हम अपना पहला ||= ऑपरेटर देखते हैं। value इस चरण में फॉल्सी है इसलिए हम दाएं हाथ की ओर ("test") का मूल्यांकन करते हैं और परिणाम को value को असाइन करते हैं। अब हम दूसरे ||= ऑपरेटर को हिट करते हैं, लेकिन इस बार value ट्रूथी है क्योंकि इसका मान "test" है। हम दाएं हाथ की ओर के मूल्यांकन को छोड़ देते हैं और value को अपरिवर्तित जारी रखते हैं।
मेमोइज़ेशन का उपयोग कब करना है यह तय करना
मेमोइज़ेशन का उपयोग करते समय हमें खुद से कुछ प्रश्न पूछने की आवश्यकता है: मान को कितनी बार एक्सेस किया जाता है? इसे क्या बदलने का कारण बनता है? यह कितनी बार बदलता है?
यदि मान को केवल एक बार एक्सेस किया जाता है तो मान को कैश करना बहुत उपयोगी नहीं होगा, मान जितनी बार एक्सेस किया जाता है, उतना अधिक लाभ हम इसे कैश करने से प्राप्त कर सकते हैं।
जब बात आती है कि इसे क्या बदलने का कारण बनता है, तो हमें मेथड में उपयोग किए जाने वाले मानों को देखने की आवश्यकता है। क्या यह तर्क लेता है? यदि हां तो मेमोइज़ेशन को शायद इसे ध्यान में रखने की आवश्यकता है। व्यक्तिगत रूप से, मुझे इसके लिए memoist जेम का उपयोग करना पसंद है क्योंकि यह आपके लिए तर्कों को संभालता है।
अंत में, हमें यह विचार करने की आवश्यकता है कि मान कितनी बार बदलता है। क्या ऐसे इंस्टेंस वेरिएबल्स हैं जो इसे बदलने का कारण बनते हैं? क्या हमें कैश्ड मान को साफ़ करने की आवश्यकता है जब वे बदलते हैं? क्या मान को ऑब्जेक्ट लेवल पर या क्लास लेवल पर कैश किया जाना चाहिए?
इन सवालों का जवाब देने के लिए आइए एक सरल उदाहरण देखें और निर्णयों के माध्यम से चरण-दर-चरण जाएं:
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
आप अक्सर क्लास-लेवल मेमोइज़ेशन नहीं चाह सकते हैं, लेकिन यह एक विकल्प के रूप में है। हालांकि, अगर आपको इस स्तर पर किसी मान को कैश करने की आवश्यकता है तो यह शायद रेडिस या मेमकैश्ड जैसे बाहरी स्टोर के साथ मान को कैश करने पर विचार करने लायक है।
रूबी ऑन रेल्स एप्लिकेशन में सामान्य मेमोइज़ेशन उपयोग के मामले
रेल्स एप्लिकेशन में, मेमोइज़ेशन के लिए सबसे आम उपयोग-मामला जो मैं देखता हूं वह है डेटाबेस कॉल को कम करना, विशेष रूप से जब एक मान एक ही अनुरोध के भीतर बदलने वाला नहीं है। कंट्रोलर में रिकॉर्ड देखने के लिए "फाइंडर" मेथड्स इस तरह के डेटाबेस कॉल का एक अच्छा उदाहरण हैं जैसे:
def current_user
@current_user ||= User.find(params[:user_id])
end
एक और आम जगह है अगर आप व्यूज़ रेंडर करने के लिए किसी प्रकार के डेकोरेटर/प्रेजेंटर/व्यू-मॉडल प्रकार की आर्किटेक्चर का उपयोग करते हैं। इन ऑब्जेक्ट्स में मेथड्स में अक्सर मेमोइज़ेशन के लिए अच्छे उम्मीदवार होते हैं क्योंकि वे केवल अनुरोध के जीवन के लिए बने रहते हैं, डेटा आमतौर पर म्यूटेट नहीं होता है, और व्यूज़ रेंडर करते समय कुछ मेथड्स शायद कई बार हिट होते हैं।
मेमोइज़ेशन के नुकसान
सबसे बड़े नुकसानों में से एक है जब यह वास्तव में आवश्यक नहीं है तब चीजों को मेमोइज़ करना। स्ट्रिंग इंटरपोलेशन मेमोइज़ेशन के लिए आसान उम्मीदवारों की तरह दिख सकते हैं, लेकिन वास्तव में, वे आपकी साइट के प्रदर्शन पर कोई ध्यान देने योग्य प्रभाव डालने की संभावना नहीं रखते हैं (जब तक कि आप असाधारण रूप से बड़ी स्ट्रिंग्स का उपयोग नहीं कर रहे हैं या बहुत बड़ी मात्रा में स्ट्रिंग मैनिपुलेशन नहीं कर रहे हैं), उदाहरण के लिए:
def title
# यहां मेमोइज़ेशन का हमारे प्रदर्शन पर ज्यादा प्रभाव नहीं पड़ेगा
@title ||= "#{@object.published_at} - #{@object.title}"
end
एक और चीज जिस पर नजर रखनी है वह है हमारा पुराना दोस्त कैश अमान्यता, विशेष रूप से यदि आपका मेमोइज़्ड मान ऑब्जेक्ट की स्थिति पर निर्भर करता है। इसे रोकने में मदद करने का एक तरीका सबसे निचले स्तर पर कैश करना है जिसे आप कर सकते हैं। a + b की गणना करने वाले एक मेथड को कैश करने के बजाय a और b मेथड्स को व्यक्तिगत रूप से कैश करना बेहतर हो सकता है।
# इसके बजाय
def profit
# कोई अन्य जो 'revenue' या 'losses' कॉल कर रहा है, यहां कैशिंग से लाभ नहीं उठा रहा है
# और क्या होगा अगर 'revenue' या 'losses' मान बदलता है, क्या हम profit को अपडेट करना याद रखेंगे?
@profit ||= (revenue - losses)
end
# इसे आज़माएं
def profit
# अब कैश्ड नहीं, लेकिन घटाव एक तेज़ गणना है
revenue - losses
end
def revenue
@revenue ||= Invoice.all.sum(:amount)
end
def losses
@losses ||= Purchase.all.sum(:amount)
end
आखिरी नुकसान यह है कि आलसी मूल्यांकन कैसे काम करता है - यदि आपको फॉल्सी मान (यानी nil या false) को मेमोइज़ करने की आवश्यकता है तो आपको कुछ अधिक कस्टम करना होगा, क्योंकि ||= मुहावरा हमेशा दाएं हाथ की ओर निष्पादित करेगा यदि आपका सहेजा गया मान फॉल्सी है। मेरे अनुभव में, इन मानों को कैश करने की आवश्यकता अक्सर नहीं होती है, लेकिन यदि आप करते हैं, तो आपको यह इंगित करने के लिए एक बूलियन फ्लैग जोड़ने की आवश्यकता हो सकती है कि यह पहले ही गणना की जा चुकी है, या किसी अन्य कैशिंग तंत्र का उपयोग करें।
def last_post
# यदि यूज़र के पास कोई पोस्ट नहीं है, तो हम हर बार इस मेथड को कॉल करने पर डेटाबेस को हिट करेंगे
@last_post ||= Post.where(user: current_user).order_by(created_at: :desc).first
end
# एक सरल वर्कअराउंड के रूप में हम कुछ ऐसा कर सकते थे:
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
जब मेमोइज़ेशन पर्याप्त नहीं है
मेमोइज़ेशन आपके एप्लिकेशन के कुछ हिस्सों में प्रदर्शन में सुधार करने का एक सस्ता और प्रभावी तरीका हो सकता है, लेकिन यह इसकी कमियों के बिना नहीं है। एक बड़ा दोष पर्सिस्टेंस है; सामान्य इंस्टेंस-लेवल मेमोइज़ेशन के लिए, मान केवल उस एक विशेष ऑब्जेक्ट के लिए सहेजा जाता है। यह मेमोइज़ेशन को वेब अनुरोध के जीवन के लिए मानों को सहेजने के लिए बहुत अच्छा बनाता है, लेकिन यदि आपके पास ऐसे मान हैं जो कई अनुरोधों के लिए समान होंगे और हर बार पुनर्गणना किए जा रहे हैं तो आपको कैशिंग का पूरा लाभ नहीं देता है।
क्लास-लेवल मेमोइज़ेशन इससे मदद कर सकता है, लेकिन कैश अमान्यता को प्रबंधित करना अधिक कठिन हो जाता है। यह उल्लेख नहीं करने के लिए कि यदि आपका सर्वर रीबूट होता है तो वे कैश्ड मान खो जाएंगे, और उन्हें कई वेब सर्वरों के बीच साझा नहीं किया जा सकता है।
इस कैशिंग श्रृंखला के अगले अंक में हम रेल्स के इन समस्याओं के समाधान को देखेंगे - लो-लेवल कैशिंग। जो आपको मानों को एक बाहरी स्टोर में कैश करने की अनुमति देता है जिसे सर्वरों के बीच साझा किया जा सकता है, और समाप्ति समय सीमा और डायनामिक कैश कुंजियों के साथ कैश अमान्यता का प्रबंधन करता है।