সবাই ইদানীং রুবির পারফরম্যান্স নিয়ে কথা বলছে, এবং সঙ্গত কারণে। দেখা যাচ্ছে যে আপনার কোডে কিছু ছোটখাট পরিবর্তনের মাধ্যমে 99.9% পর্যন্ত কর্মক্ষমতা বৃদ্ধি করা সম্ভব।
কীভাবে সেখানে প্রচুর নিবন্ধ রয়েছে৷ আপনার কোড অপ্টিমাইজ করতে, কিন্তু কিভাবে আপনি নিশ্চিত করতে পারেন যে আপনার কোড রয়ে গেছে অপ্টিমাইজ করা হয়েছে?
একটি নিয়মিত বলা পদ্ধতিতে একটি হিমায়িত ধ্রুবকের পরিবর্তে একটি স্ট্রিং আক্ষরিক এম্বেড করার সময় আপনি সবসময় ফলাফলগুলি বিবেচনা নাও করতে পারেন -- ভবিষ্যতে আপনার কোড বজায় রাখার সময় আপনার অপ্টিমাইজেশনের সঞ্চয় হারানো খুব সহজ৷
এইগুলি আমার চিন্তাভাবনা ছিল সম্প্রতি যখন আমি হানিব্যাজারে আমাদের রুবি রত্নটিতে দ্বিতীয় (বা তৃতীয়) বারের জন্য কিছু কোড অপ্টিমাইজ করেছি:"এই অপ্টিমাইজেশানগুলি যাতে রিগ্রেস না হয় তা নিশ্চিত করার একটি উপায় থাকলে এটি কি ভাল হবে না৷ ?"
নাম না থাকলেও সফ্টওয়্যার ডেভেলপমেন্টে রিগ্রেশন এমন কিছু যা আমাদের মধ্যে বেশিরভাগই পরিচিত। একটি রিগ্রেশন ঘটে যখন একটি বাগ বা সমস্যা যা অতীতে সমাধান করা হয়েছিল একই কোডে ভবিষ্যতের পরিবর্তনের কারণে পুনরায় ঘটে। একই কাজ একবারের বেশি করতে কেউ পছন্দ করে না; প্রত্যাবর্তন হল মেঝেতে ময়লা ট্র্যাক করার মতন এটি ঝাড়ু দেওয়ার পরে।
ভাগ্যক্রমে, আমাদের কাছে একটি গোপন অস্ত্র রয়েছে:পরীক্ষা। আপনি ডগম্যাটিক TDD অনুশীলন করুন বা না করুন, পরীক্ষাগুলি অসাধারণ বাগ ফিক্স করার জন্য কারণ তারা সমস্যা এবং সমাধান প্রোগ্রাম্যাটিকভাবে প্রদর্শন করে। পরীক্ষাগুলি আমাদের আত্মবিশ্বাস দেয় যে পরিবর্তনগুলি ঘটলে রিগ্রেশন ঘটবে না৷
৷পরিচিত শব্দ? আমিও তাই ভেবেছিলাম, যা আমাকে আশ্চর্য করেছে, "যদি পারফরম্যান্স অপ্টিমাইজেশান রিগ্রেশন করতে পারে, তাহলে আমি কেন পরীক্ষার মাধ্যমেও সেই রিগ্রেশনগুলি ধরতে পারি না?"
অবজেক্ট অ্যালোকেশন, মেমরি, সিপিইউ, আবর্জনা সংগ্রহ ইত্যাদি সহ রুবির বিভিন্ন পারফরম্যান্সের দিকগুলি প্রোফাইল করার জন্য অনেকগুলি দুর্দান্ত সরঞ্জাম রয়েছে৷ এর মধ্যে কিছু রুবি-প্রোফ, স্ট্যাকপ্রফ এবং অ্যালোকেশন_ট্র্যাসার অন্তর্ভুক্ত৷
আমি সম্প্রতি প্রোফাইল অবজেক্ট বরাদ্দের জন্য allocation_stats ব্যবহার করছি। বরাদ্দ হ্রাস করা একটি মোটামুটি সহজ কাজ, যা মেমরি খরচ এবং গতি সুরক্ষিত করার জন্য অনেক কম ঝুলন্ত ফল দেয়৷
উদাহরণস্বরূপ, এখানে একটি মৌলিক রুবি ক্লাস রয়েছে যা 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টি স্ট্রিং বরাদ্দ করা হচ্ছে , যা অপ্রয়োজনীয় বলে মনে হয় যেহেতু আমি জানি তারা সব একই মান ধারণ করে। আমি আমার দৃশ্যকল্পটি এমন কিছু পড়তে চেয়েছিলাম:"মাইক্লাস শুরু করার সময় এটি 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
আমি LoadError
উদ্ধার করছি প্রাথমিক প্রয়োজন বিবৃতিতে কারণ আমি প্রতিটি পরীক্ষা চালানোর সময় বরাদ্দ পরিসংখ্যান অন্তর্ভুক্ত করতে চাই না (এটি পরীক্ষাগুলিকে ধীর করে দেয়)। আমি তারপর :allocate_under
সংজ্ঞায়িত করি ম্যাচার যা match
এর ভিতরে ট্রেস সম্পাদন করে ব্লক failure_message
ব্লকটিও গুরুত্বপূর্ণ কারণ এতে to_text
অন্তর্ভুক্ত রয়েছে AllocationStats ট্রেস থেকে আউটপুট ঠিক আমার ব্যর্থতা বার্তার ভিতরে ! বাকি ম্যাচার বেশিরভাগই স্ট্যান্ডার্ড 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
ঠিক আছে, তাই আমি প্রোগ্রাম্যাটিকভাবে পারফরম্যান্সের সমস্যাটি প্রদর্শন করেছি, যা মাইক্লাস একই মান সহ অতিরিক্ত স্ট্রিং অবজেক্ট বরাদ্দ করে। আসুন সেই মানগুলিকে একটি হিমায়িত ধ্রুবকের মধ্যে নিক্ষেপ করে সেই সমস্যাটি সমাধান করি:
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
পরিবর্তন করব পদ্ধতি, আমি আত্মবিশ্বাসী হতে পারি যে আমি খুব বেশি বস্তু বরাদ্দ করছি না।
কারণ প্রোফাইলিং বরাদ্দ তুলনামূলকভাবে ধীর হতে পারে, এটি সব সময় চেয়ে চাহিদা অনুযায়ী চালানো আদর্শ হবে। যেহেতু আমি ইতিমধ্যেই অনুপস্থিত বরাদ্দ_পরিসংখ্যানকে সুন্দরভাবে পরিচালনা করছি, আমি একাধিক জেমফাইল তৈরি করতে বান্ডলার ব্যবহার করতে পারি এবং তারপরে BUNDLE_GEMFILE এনভায়রনমেন্ট ভেরিয়েবলের সাথে কোন জেমফাইলটি ব্যবহার করতে চাই তা উল্লেখ করতে পারি:
$ BUNDLE_GEMFILE=with_performance.gemfile bundle exec rspec spec/
$ BUNDLE_GEMFILE=without_performance.gemfile bundle exec rspec spec/
আরেকটি বিকল্প হল মূল্যায়ন রত্ন-এর মতো একটি লাইব্রেরি ব্যবহার করা, যা একই পদ্ধতি গ্রহণ করে এবং কিছু বান্ডলার গোটচাস সমাধান করে। জেসন ক্লার্ক মার্চ 2015-এ রুবি অন অ্যালেসে কীভাবে এটি করা যায় তার একটি চমৎকার উপস্থাপনা দিয়েছেন; আরও জানতে তার স্লাইডগুলি দেখুন৷
৷আমি আরও মনে করি যে আমার সাধারণ ইউনিট পরীক্ষাগুলি থেকে আলাদাভাবে এই ধরণের পরীক্ষাগুলি বজায় রাখা একটি ভাল ধারণা, তাই আমি একটি নতুন "পারফরম্যান্স" ডিরেক্টরি তৈরি করব যাতে আমার ইউনিট টেস্ট স্যুট spec/unit/-এ থাকে এবং আমার পারফরম্যান্স স্যুটটি স্পেকের মধ্যে থাকে /কর্মক্ষমতা/:
spec/
|-- spec_helper.rb
|-- unit/
|-- features/
|-- performance/
আমি এখনও পারফরম্যান্সের জন্য রুবি কোড প্রোফাইল করার জন্য আমার পদ্ধতিকে পরিমার্জন করছি; আমি আশা করি যে একটি পারফরম্যান্স টেস্ট স্যুট বজায় রাখা আমাকে এখন আমার কোডের গতি উন্নত করতে, ভবিষ্যতে এটিকে দ্রুত রাখতে এবং নিজের এবং অন্যদের জন্য ডকুমেন্টেশন তৈরি করতে সাহায্য করবে।