বেঞ্চমার্কিং

বেঞ্চমার্কিং #

অধ্যায়ের নামটি দেখে আমাদের অনেকের ভ্রুই হয়তো দুই হাত কুঁচকে উঠেছে। বেঞ্চমার্কিং? এটা আবার কোন চিজ?

উইকিপিডিয়া বলে-

কতগুলো স্ট্যান্ডার্ড টেস্ট ও ট্রায়াল চালিয়ে কোনো অবজেক্টের রিলেটিভ পারফরম্যান্স (আপেক্ষিক কর্মক্ষমতা) মূল্যায়ন করতে কোনো কম্পিউটার প্রোগ্রাম বা কয়েকটা প্রোগ্রাম অথবা অন্য কোনো অপারেশন চালানোকেই বেঞ্চমার্কিং বলে।

আরও সহজভাবে বলা যায়, একটা প্রোগ্রাম কতটুকু দ্রুত চলে বা কোথায় এর চলার গতি ব্যাহত হয়, তা খুঁজে বের করার প্রক্রিয়াই হলো বেঞ্চমার্কিং। সাধারণত চারটি প্রশ্নের উত্তর খুঁজে পেতে একটা প্রোগ্রামের বেঞ্চমার্কিং করা হয়-

  • এটি কত দ্রুত চলে?
  • কোথায় এর গতি ব্যাহত হচ্ছে?
  • কতটুকু মেমোরি এটি ব্যবহার করছে?
  • কোথায় মেমোরি লিকিং হচ্ছে অথবা কোন ব্লকটা বেশি মেমোরি খাচ্ছে (ব্যবহার করছে)?

আমরা এখন এই চার প্রশ্নের উত্তর খুঁজে বের করতে কিছু টুলসের সহায়তা নেব। তবে তার আগে test.py-তে আমরা নিচের মতো করে একটা প্রোগ্রাম লিখে নেব। এই প্রোগ্রামটার ওপরেই আমরা কারুকাজ চালাব।

time #

লিনাক্সের মান্ধাতার আমলের টাইম টুল দিয়েই শুরু করা যাক। সম্ভবত এটি ‘ম্যাকওএস’-এও আছে। এই টুল ব্যবহার করে একটি প্রোগ্রাম চলতে কোন খাতে কতটুকু সময় ব্যয় করে তার হিসাব পাওয়া যায়। আমাদের test.py-এর বেঞ্চমার্কিং করতে নিচের মতো করে টার্মিনালে কমান্ড দেব।

$ time -v python3 test.py

এ ক্ষেত্রে আমরা নিচের মতো কিছু আউটপুট পাব—

100000 loops.
Command being timed: "python3 test.py"
User time (seconds): 0.05
System time (seconds): 0.00
Percent of CPU this job got: 100%
Elapsed (wall clock) time (h:mm:ss or m:ss): 0:00.05
Average shared text size (kbytes): 0
Average unshared data size (kbytes): 0
Average stack size (kbytes): 0
Average total size (kbytes): 0
Maximum resident set size (kbytes): 9392
Average resident set size (kbytes): 0
Major (requiring I/O) page faults: 0
Minor (reclaiming a frame) page faults: 1105
Voluntary context switches: 0
Involuntary context switches: 8
Swaps: 0
File system inputs: 0
File system outputs: 0
Socket messages sent: 0
Socket messages received: 0
Signals delivered: 0
Page size (bytes): 4096
Exit status: 0

সবটুকু আমাদের মাথাব্যথার কারণ নয়। কেবল User time, System time ও Elapsed time নিয়ে আমরা চিন্তা করব। এই তিন ধরনের সময়ের ভিন্ন ভিন্ন অর্থ আছে।

  • ইউজার টাইম (User time): কার্নেলের বাইরে খরচ হওয়া সিপিইউ টাইম। অর্থাৎ শুধু প্রোগ্রামটা এক্সিকিউট হতে যে সিপিইউ টাইম লাগে, সে সময়টা। I/O ওয়েটিং টাইমের মধ্যে পড়ে না।
  • সিস্টেম টাইম (System time): সিস্টেম টাইম হলো, কার্নেলের নির্দিষ্ট কিছু ফাংশনের মধ্যে খরচ হওয়া সিপিইউ টাইম। অর্থাৎ কার্নেলের ভেতরে সিস্টেম কলে খরচ হওয়া সিপিইউ টাইম।
  • ইলাপসড টাইম (Elapsed time): এটা হলো আসল, একেবারে অরিজিনালি খরচ হওয়া ওয়াল ক্লক টাইম। প্রোগ্রাম শুরু হওয়া থেকে একেবারে শেষ পর্যন্ত- অন্যান্য প্রসেসের টাইম স্লাইস, I/O ওয়েটিং টাইম- সবকিছু মিলেই ইলাপসড টাইম।

এসব উপাত্ত থেকে প্রোগ্রামের পারফরম্যান্স সম্পর্কে তথ্য পাওয়া যায়। যদি System time ও User time-এর সমষ্টি Elapsed time-এর চেয়ে বেশি ছোট হয় তাহলে ধরে নেওয়া I/Oতে প্রোগ্রামের পারফরম্যান্স ফল করছে।

timeit #

পাইথন স্ট্যান্ডার্ড লাইব্রেরির একটা মডিউল হলো টাইমইট। কমান্ডলাইন ইন্টারফেস ও পাইথন ইন্টারফেস দুটিই সাপোর্ট করে। পাইথনের ছোট ছোট কোড স্নিপেট কতটুকু সময় খাচ্ছে তা হিসাব করা যায় এই মডিউল দিয়ে। আর প্ল্যাটফর্ম স্পেসিফিক টাইম ফাংশন ব্যবহার করে বলে সবচেয়ে নির্ভুল ফলাফল পাওয়া যায়। উদাহরণ দেখা যাক:

$ python3 -m timeit '"-".join(str(n) for n in range(100))'

আউটপুট

10000 loops, best of 3: 41.5 usec per loop

এখানে python3-এর পরে -m আর্গুমেন্ট দিয়ে আমরা পাইথনকে বলছি টাইমইট মডিউলের খোঁজ নিতে ও এটাকে মেইন প্রোগ্রাম হিসেবে ব্যবহার করতে। -s আর্গুমেন্ট টাইমইট মডিউলে বলছে সেটআপকে একবার রান করতে। তারপর এটা আমাদের কোডকে রান করে। আউটপুট থেকে আমরা দেখতে পাচ্ছি যে আমাদের কোডকে তিনবার রান করা হয়েছে এবং বেস্ট অ্যাভারেজকে আউটপুট হিসেবে দেখানো হয়েছে। ওপরের এই একই কাজ আমরা পাইথন ইন্টারেক্টিভ শেলেও করতে পারি।

>>> import timeit
>>> timeit.timeit('"-".join(str(n) for n in range(100))', number=10000)
0.41126898900074593

আউটপুটে একটু হেরফের হয়েছে। আসলে কেবল কমান্ডলাইন ইন্টারফেসেই টাইমইট নিজ থেকে রিপিটেশনের সংখ্যা ঠিক করে নেয়।

cProfile #

পাইথন স্ট্যান্ডার্ড লাইব্রেরির আরেকটি বেঞ্চমার্কিং মডিউল হলো সিপ্রোফাইল। এটা আসলে একটা সি এক্সটেনশন (সাথে আরও কিছু)। আমাদের test.py ফাইলের প্রোগ্রামকে বেঞ্চমার্কিং করতে আমরা টার্মিনালে নিচের কমান্ডটা দেব।

$ python3 -m cProfile test.py

আউটপুট

100000 loops.
        5 function calls in 0.010 seconds

   Ordered by: standard name

  ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.010    0.010  test.py:1(<module>)
        1    0.010    0.010    0.010    0.010  test.py:1(ghoraghuri)
        1    0.000    0.000    0.010    0.010  {built-in method builtins.exec}
        1    0.000    0.000    0.000    0.000  {built-in method builtins.print}
        1    0.000    0.000    0.000    0.000  {method 'disable' of '_lsprof.Profiler' objects}

এর আউটপুটে বোঝার মতো কিছু বিষয় আছে। যেমন-

  • ncalls: কল সংখ্যা।
  • tottime: এই ফাংশনে মোট ব্যয়কৃত সময় (সাব-ফাংশন হিসেবের বাইরে)।
  • percall: প্রত্যেক কলে ব্যয়কৃত সময়। tottime কে ncalls দিয়ে ভাগ দিলে পাওয়া যায়।
  • cumtime: প্রোগ্রামের শুরু থেকে শেষ পর্যন্ত মোট ব্যয়কৃত সময় (সাব-ফাংশনসহ)।
  • percall: cumtime কে ncalls দিয়ে ভাগ দিলে পাওয়া যায়।

profilehooks #

প্রোফাইলহুকসও একটি থার্ড-পার্টি মডিউল। এই মডিউলটি আসলে কয়েকটি প্রোফাইলিং ফাংশনের কালেকশন। আরও ক্লিয়ার করে বলতে গেলে পাইথন স্ট্যান্ডার্ড লাইব্রেরির টাইমইট এবং সিপ্রোফাইল মডিউলকে নিয়েই এখানের কাজগুলো। শুরুতেই ব্যবহার করার মডিউলটি আমরা ইনস্টল করব, সে জন্য নিচের মতো করে কমান্ড দেব-

$ sudo pip3 install profilehooks

ইনস্টল হয়ে গেল। এবার আমরা @profile ডেকোরেটরটাকে ব্যবহার করব। এ জন্য আমাদের test.py ফাইলের প্রোগ্রামটাকে প্রয়োজন অনুযায়ী রিরাইট করব।

from profilehooks import profile

@profile
def ghoraghuri():
    count = 0
    while count < 100000:
        count += 1
    return count

if __name__ == '__main__':
    temp = ghoraghuri()
    print(temp, 'loops.')

** আউটপুট **

100000 loops.

*** PROFILER RESULTS ***
ghoraghuri (test.py:3)
function called 1 times

        2 function calls in 0.010 seconds

   Ordered by: cumulative time, internal time, call count

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1   0.010    0.010    0.010    0.010  test.py:3(ghoraghuri)
        1   0.000    0.000    0.000    0.000  {method 'disable' of '_lsprof.Profiler' objects}
        0    0.000    0.000        profile:0(profiler)

পরিচিত মনে হচ্ছে? এখানে সিপ্রোফাইল মডিউলটিকে ব্যবহার করা হয়েছে। এবার আমরা @timecall ডেকোরেটরটাকে ব্যবহার করব-

from profilehooks import timecall

@timecall
def ghoraghuri():
    count = 0
    while count < 100000:
        count += 1
    return count

if __name__ == '__main__':
    temp = ghoraghuri()
    print(temp, 'loops.')

আউটপুট

ghoraghuri (test.py:3):
    0.009 seconds

100000 loops.

এই রেজাল্টটাও পরিচিত লাগছে? আসলে আমরা এখানে timeit মডিউলেরই ব্যবহার করেছি গোপনে। যাহোক, এবার আমরা @coverage ডেকোরেটরের ব্যবহার করব-

from profilehooks import coverage

@coverage
def ghoraghuri():
    count = 0
    while count < 100000:
        count += 1
    return count

if __name__ == '__main__':
    temp = ghoraghuri()
    print(temp, 'loops.')

আউটপুট

100000 loops.

*** COVERAGE RESULTS ***
ghoraghuri (test.py:3)
function called 1 times

                  @coverage
                  def ghoraghuri():
       1:         count = 0
100001:        while count < 100000:
100000:             count += 1
       1:         return count

এই হলো এই মডিউলের ব্যবহার। বিস্তারিতভাবে জানার জন্য এই লিঙ্কে ঘুরে আসতে পারেন।

line_profiler #

এটি পাইথনের একটি থার্ডপার্টি মডিউল। এটি ব্যবহার করে খুব সহজেই একটি প্রোগ্রামের কোন লাইন কত সময় খাচ্ছে, তা দেখা যায়। প্রথমেই ইনস্টল করে নেওয়া যাক মডিউলটা।

$ sudo pip3 install line_profiler

এবার আমরা আমাদের test.py ফাইলের বেঞ্চমার্কিং করব। তবে তার আগে test.py ফাইলের প্রোগ্রামটাকে নিচের মতো রিরাইট করতে হবে-

@profile
def ghoraghuri():
    count = 0
    while count < 100000:
        count += 1
    return count

if __name__ == '__main__':
    temp = ghoraghuri()
    print(temp, 'loops.')

আমরা যেসব ফাংশনের চুলচেরা বিশ্লেষণ করব, তাদের আগে @profile ডেকোরেটর ব্যবহার করতে হবে। এবার টার্মিনালে কমান্ড চালানো যাক-

$ kernprof -l -v test.py

আউটপুট

100000 loops.
Wrote profile results to test.py.lprof
Timer unit: 1e-06 s

Total time: 0.111737 s
File: test.py
Function: ghoraghuri at line 1

Line#	Hits	Time	Per Hit	%Time	Line Contents
===============================================================
	1					@profile
	2					def ghoraghuri():
	3	1	2	2.0	0.0	count = 0
	4	100001	55342	0.6	49.5	while count < 100000:
	5	100000	56392	0.6	50.5	count += 1
	6	1	1	1.0	0.0	return count

ক্ষেত্রগুলো একটু ব্যাখ্যা করা যাক।

  • Line#: কোডের লাইন নম্বর, যেটাকে চুলচেরা বিশ্লেষণ করা হয়েছে।
  • Hits: যে কয়বার এই লাইনটি এক্সিকিউট হয়েছে।
  • Time: লাইনটি এক্সিকিউট হতে মোট যতক্ষণ সময় নিয়েছে।
  • Per Hit: লাইনটি এক্সিকিউট হতে গড়ে প্রতিবার যত সময় নিয়েছে।
  • % Time: টোটাল সময়ের পরিপ্রেক্ষিতে শতকরা কতক্ষণ লাইনটা এক্সিকিউট হয়েছে।

memory_profiler #

মেমোরি প্রোফাইলারও একটি থার্ড-পার্টি মডিউল যা মেমোরি কনজাম্পশন করার জন্য ব্যবহৃত হয়। এই মডিউল ব্যবহার করে লাইন বাই লাইন পাইথন প্রোগ্রামের মেমোরি কনজাম্পশন অ্যানালাইজ করা যায়। সুতরাং প্রথমেই মডিউলটি ইনস্টল করে নেওয়া যাক-

$ sudo pip3 install memory_profiler
$ sudo pip3 install psutil

ইনস্টল হয়ে গেল। লাইন প্রোফাইলারের ক্ষেত্রে আমরা যেভাবে আমাদের প্রোগ্রামটা রিরাইট করেছিলাম, এখানে ঠিক একইভাবে @profile ডেকোরেটর বসাতে হবে ফাংশনের আগে। যেহেতু আমরা ইতিমধ্যে test.py ফাইলে আমাদের প্রোগ্রাম রিরাইট করে রেখেছি তাই আমরা সরাসরি অ্যানালিসিসে চলে যাব।

$ python3 -m memory_profiler test.py

আউটপুট

100000 loops.
Filename: test.py

Line #	Mem usage	Increment	Line Contents
===============================================================
	1	31.648 MiB	0.000 MiB	@profile
	2			def ghoraghuri():
	3	31.648 MiB	0.000 MiB	count = 0
	4	31.648 MiB	0.000 MiB	while count < 100000:
	5	31.648 MiB	0.000 MiB	count += 1
	6	31.648 MiB	0.000 MiB	return count

এই মডিউলটির আরও অনেক ফাংশনালিটি আছে। বিস্তারিত জানার জন্য এই লিঙ্কে ঘুরে আসতে পারেন।

pycallgraph #

কমান্ডলাইনে আউটপুট তো অনেক পেলাম। এবার একটু ভিজ্যুলাইজেশন যাওয়া যাক। এ জন্য আমরা পাইকলগ্রাফ মডিউলটি ব্যবহার করব।

sudo pip3 install pycallgraph
sudo apt-get install graphviz -y

ইনস্টল হয়ে গেলে এবার আমরা নিচের কমান্ডটা দেব-

$ pycallgraph graphviz -- ./test.py

কিন্তু এখানে শুধু প্রোগ্রামের আউটপুটটি দেখতে পাব, কোনো বেঞ্চমার্ক ইনফরমেশন দেখতে পাব না। দুঃখ পাওয়ার কিছু নেই। test.py-এর ডিরেক্টরিতে গিয়ে দেখব pycallgraph.png নামে কোনো ফাইল আছে কি না। এই ফাইলটা ওপেন করলেই আমরা সাত রাজার ধন পেয়ে যাব।

আরও জানার জন্য পাইকলগ্রাফের অফিশিয়াল ডকুমেন্টেশন ফলো করুন।

মন্তব্য করুন