हर कोई हाल ही में Ruby प्रदर्शन के बारे में बात कर रहा है, और अच्छे कारण के साथ। यह पता चला है कि आपके कोड में कुछ छोटे बदलावों के साथ प्रदर्शन को 99.9% तक बढ़ाना संभव है।
आपके कोड को *कैसे* अनुकूलित करें, इस पर बहुत सारे आर्टिकल हैं, लेकिन आप कैसे सुनिश्चित कर सकते हैं कि आपका कोड *अनुकूलित रहता है*?
आप नियमित रूप से कॉल की जाने वाली मेथड में एक फ्रोज़न कॉन्स्टेंट के बजाय एक स्ट्रिंग लिटरल एम्बेड करने के परिणामों पर हमेशा विचार नहीं कर सकते -- भविष्य में अपने कोड को बनाए रखते समय आपके अनुकूलन की बचत को खोना बहुत आसान है।
ये मेरे हाल के विचार थे जब मैंने Honeybadger पर हमारे Ruby जेम में दूसरी (या तीसरी) बार कुछ कोड को अनुकूलित किया: "क्या यह बहुत अच्छा नहीं होगा यदि यह सुनिश्चित करने का कोई तरीका होता कि ये अनुकूलन *पुनरावृत्ति* न करें?"
रिग्रेशन कुछ ऐसा है जिससे हम में से अधिकांश सॉफ्टवेयर डेवलपमेंट में परिचित हैं, भले ही नाम से नहीं। एक रिग्रेशन तब होता है जब अतीत में हल किया गया बग या मुद्दा भविष्य में उसी कोड में बदलाव के कारण फिर से होता है। कोई भी एक ही काम एक से अधिक बार करना पसंद नहीं करता; रिग्रेशन ठीक उस गंदगी को ट्रैक करने जैसा है जिसे अभी-अभी साफ किया गया है।
सौभाग्य से, हमारे पास एक गुप्त हथियार है: टेस्ट। चाहे आप डॉगमैटिक TDD का अभ्यास करें या नहीं, टेस्ट बग्स को ठीक करने के लिए *बहुत बढ़िया* हैं क्योंकि वे प्रोग्रामेटिक रूप से मुद्दे और समाधान को प्रदर्शित करते हैं। टेस्ट हमें विश्वास दिलाते हैं कि जब बदलाव होते हैं तो रिग्रेशन नहीं होंगे।
परिचित लगता है? मुझे भी ऐसा ही लगा, जिसने मुझे आश्चर्यचकित कर दिया, "यदि प्रदर्शन अनुकूलन रिग्रेस हो सकते हैं, तो मैं उन रिग्रेशन को टेस्ट के साथ क्यों नहीं पकड़ सकता?"
Ruby के विभिन्न प्रदर्शन पहलुओं को प्रोफाइल करने के लिए बहुत सारे बेहतरीन टूल हैं जिनमें ऑब्जेक्ट आवंटन, मेमोरी, CPU, गार्बेज कलेक्शन, आदि शामिल हैं। इनमें से कुछ में ruby-prof, stackprof और allocation_tracer शामिल हैं।
मैं हाल ही में ऑब्जेक्ट आवंटन को प्रोफाइल करने के लिए allocation_stats का उपयोग कर रहा हूँ। आवंटन कम करना एक काफी आसान कार्य है, जो मेमोरी खपत और गति को ट्यून करने के लिए बहुत सारे लो-हैंगिंग फ्रूट देता है।
उदाहरण के लिए, यहाँ एक बुनियादी Ruby क्लास है जो 5 स्ट्रिंग्स की एक सरणी संग्रहीत करती है जो डिफ़ॉल्ट रूप से 'foo' हैं:
class MyClass def initialize @values = Array.new(5) 5.times { @values << 'foo' } end end
AllocationStats API सरल है। इसे प्रोफाइल करने के लिए एक ब्लॉक दें, और यह प्रिंट करेगा कि सबसे अधिक ऑब्जेक्ट कहां आवंटित किए गए हैं।
$ ruby -r allocation_stats -r ./lib/my_class stats = AllocationStats.trace { MyClass.new } puts stats.allocations(alias_paths: true).group_by(:sourcefile, :sourceline, :class).to_text ^D sourcefile sourceline class count --------------------- ---------- ------- ----- /lib/my_class.rb 4 String 5 /lib/my_class.rb 3 Array 1 - 1 MyClass 1
`#to_text` मेथड (आवंटन के एक समूह पर कॉल किया गया) बस आपके द्वारा मांगे गए मानदंडों द्वारा समूहीकृत एक अच्छा मानव-पठनीय टेबल प्रिंट करता है।
यह आउटपुट मैन्युअल रूप से प्रोफाइलिंग करते समय बहुत अच्छा है, लेकिन मेरा लक्ष्य एक टेस्ट बनाना था जो मेरे सामान्य यूनिट टेस्ट सूट (जो RSpec में लिखा गया है) के साथ चल सके। हम देख सकते हैं कि my_class.rb की लाइन 4 पर, 5 स्ट्रिंग्स आवंटित की जा रही हैं, जो अनावश्यक लगता है क्योंकि मुझे पता है कि उन सभी में समान मान है। मैं चाहता था कि मेरा परिदृश्य ऐसा पढ़े: "MyClass आरंभ करते समय यह 6 से कम ऑब्जेक्ट आवंटित करता है"। RSpec में यह कुछ इस तरह दिखता है:
describe MyClass do context "when initializing" do specify { expect { MyClass.new }.to allocate_under(6).objects } end end
इस सिंटैक्स का उपयोग करके मेरे पास वर्णित कोड ब्लॉक (अंदर `expect` ब्लॉक) के लिए ऑब्जेक्ट आवंटन की एक दी गई संख्या से कम होने का परीक्षण करने के लिए आवश्यक सब कुछ है, एक कस्टम RSpec मैचर का उपयोग करके।
ट्रेस परिणाम प्रिंट करने के अलावा, AllocationStats आवंटन तक पहुंचने के लिए कुछ मेथड प्रदान करता है, जिसमें `#allocations` और `#new_allocations` शामिल हैं। ये वही हैं जिनका उपयोग मैंने अपना मैचर बनाने के लिए किया:
begin require 'allocation_stats' rescue LoadError puts 'Skipping AllocationStats.' end RSpec::Matchers.define :allocate_under do |expected| match do |actual| return skip('AllocationStats is not available: skipping.') unless defined?(AllocationStats) @trace = actual.is_a?(Proc) ? AllocationStats.trace(&actual) : actual @trace.new_allocations.size < expected end def objects self end def supports_block_expectations? true end def output_trace_info(trace) trace.allocations(alias_paths: true).group_by(:sourcefile, :sourceline, :class).to_text end failure_message do |actual| "expected under #{ expected } objects to be allocated; got #{ @trace.new_allocations.size }:\n\n" << output_trace_info(@trace) end description do "allocates under #{ expected } objects" end end
मैं प्रारंभिक require स्टेटमेंट में `LoadError` को रेस्क्यू कर रहा हूँ क्योंकि मैं हर टेस्ट रन पर AllocationStats शामिल नहीं करना चाह सकता (यह टेस्ट को धीमा कर देता है)। फिर मैं `:allocate_under` मैचर को परिभाषित करता हूँ जो `match` ब्लॉक के अंदर ट्रेस करता है। `failure_message` ब्लॉक भी महत्वपूर्ण है क्योंकि इसमें AllocationStats ट्रेस से `to_text` आउटपुट *मेरे असफलता संदेश के अंदर* शामिल है! बाकी मैचर ज्यादातर मानक RSpec कॉन्फ़िगरेशन है।
मेरा मैचर लोड होने के साथ, मैं अब अपना पिछला परिदृश्य चला सकता हूँ, और इसे विफल होते देख सकता हूँ:
$ rspec spec/my_class_spec.rb MyClass when initializing should allocates under 6 objects (FAILED - 1) Failures: 1) MyClass when initializing should allocates under 6 objects Failure/Error: expect { MyClass.new }.to allocate_under(6).objects expected under 6 objects to be allocated; got 7: sourcefile sourceline class count --------------------------- ---------- ------- ----- <PWD>/spec/my_class_spec.rb 6 MyClass 1 <PWD>/lib/my_class.rb 3 Array 1 <PWD>/lib/my_class.rb 4 String 5 # ./spec/my_class_spec.rb:6:in `block (3 levels) in <top (required)>' Finished in 0.15352 seconds (files took 0.22293 seconds to load) 1 example, 1 failure Failed examples: rspec ./spec/my_class_spec.rb:5 # MyClass when initializing should allocates under 6 objects
ठीक है, इसलिए मैंने प्रोग्रामेटिक रूप से प्रदर्शन समस्या का प्रदर्शन किया है, जो यह है कि MyClass समान मान के साथ अतिरिक्त स्ट्रिंग ऑब्जेक्ट आवंटित करता है। आइए उन मानों को एक फ्रोज़न कॉन्स्टेंट में डालकर उस समस्या को ठीक करें:
class MyClass DEFAULT = 'foo'.freeze def initialize @values = Array.new(5) 5.times { @values << DEFAULT } end end
अब जब मैंने समस्या ठीक कर दी है, तो मैं अपना टेस्ट फिर से चलाऊंगा और इसे पास होते देखूंगा:
$ rspec spec/my_class_spec.rb MyClass when initializing should allocates under 6 objects Finished in 0.14952 seconds (files took 0.22056 seconds to load) 1 example, 0 failures
अगली बार जब मैं `MyClass#initialize` मेथड बदलूंगा, तो मुझे विश्वास हो सकता है कि मैं बहुत अधिक ऑब्जेक्ट आवंटित नहीं कर रहा हूँ।
चूंकि आवंटन प्रोफाइलिंग अपेक्षाकृत धीमी हो सकती है, इसलिए इन्हें हर समय के बजाय ऑन-डिमांड चलाना आदर्श होगा। क्योंकि मैं पहले से ही allocation_stats के गायब होने को ग्रेसफुली हैंडल कर रहा हूँ, मैं बंडलर का उपयोग करके कई जेमफाइल्स बना सकता हूँ और फिर BUNDLE_GEMFILE पर्यावरण चर के साथ निर्दिष्ट कर सकता हूँ कि मैं किस जेमफाइल का उपयोग करना चाहता हूँ:
$ BUNDLE_GEMFILE=with_performance.gemfile bundle exec rspec spec/ $ BUNDLE_GEMFILE=without_performance.gemfile bundle exec rspec spec/
एक अन्य विकल्प appraisal जेम जैसी लाइब्रेरी का उपयोग करना है, जो इसी दृष्टिकोण को लेती है और कुछ बंडलर गॉटचा को हल करती है। जेसन क्लार्क ने मार्च 2015 में Ruby on Ales पर इसे कैसे करें पर एक उत्कृष्ट प्रस्तुति दी; अधिक जानने के लिए उनकी स्लाइड्स देखें।
मुझे यह भी लगता है कि इन प्रकार के टेस्ट को अपने सामान्य यूनिट टेस्ट से अलग रखना एक अच्छा विचार है, इसलिए मैं एक नया "प्रदर्शन" डायरेक्टरी बनाऊंगा ताकि मेरा यूनिट टेस्ट सूट spec/unit/ में रहे और मेरा प्रदर्शन सूट spec/performance/ में रहे:
spec/ |-- spec_helper.rb |-- unit/ |-- features/ |-- performance/
मैं अभी भी Ruby कोड को प्रदर्शन के लिए प्रोफाइल करने के अपने दृष्टिकोण को परिष्कृत कर रहा हूँ; मेरी आशा है कि एक प्रदर्शन टेस्ट सूट को बनाए रखने से मुझे अपने कोड की गति को अभी सुधारने, भविष्य में इसे तेज रखने, और अपने और दूसरों के लिए दस्तावेज़ीकरण बनाने में मदद मिलेगी।