পারফরম্যান্স বটলনেক শনাক্তকরণ
এই পাঠে যা শিখবেন
- "পরিমাপ করুন, অনুমান করবেন না" নীতি এবং কেন এটি গুরুত্বপূর্ণ
- সাধারণ বটলনেক ক্যাটাগরি: CPU-bound, I/O-bound, ডেটাবেজ, নেটওয়ার্ক
time.perf_counter()দিয়ে একটি বাস্তব, চলমান প্রোফাইলিং ডেমো- কীভাবে একই ইনপুট সাইজে ভিন্ন অ্যালগরিদমিক জটিলতা ( $O(n)$ বনাম $O(n^2)$ ) বাস্তবে বিশাল সময়ের পার্থক্য তৈরি করে
১ · পরিমাপ করুন, অনুমান করবেন না
একটি সিস্টেম ধীর হলে, স্বাভাবিক প্রবৃত্তি হলো অনুমান করা — "মনে হয় ডেটাবেজ কোয়েরিটাই সমস্যা", বা "নিশ্চয়ই
ঐ জটিল লুপটাই দোষী"। কিন্তু বাস্তবে ডেভেলপারের স্বজ্ঞা প্রায়ই ভুল প্রমাণিত হয় — যে অংশটি "জটিল" বা "সন্দেহজনক"
মনে হয়, সেটি আদৌ সবচেয়ে বেশি সময় না-ও নিতে পারে। প্রোফাইলিং — প্রকৃতপক্ষে প্রতিটি ধাপের সময়
মেপে দেখা — অনুমানের বদলে নিশ্চিত প্রমাণ দেয়। বাস্তব জগতে এর জন্য Python-এর নিজস্ব cProfile
মডিউল, বা প্রোডাকশন সিস্টেমে APM (Application Performance Monitoring) টুল ব্যবহার করা হয়; এই পাঠের কোড সেল
সেই একই মূলনীতি — সময় মাপা, অনুমান নয় — একটি ছোট, হাতে-কলমে দেখার মতো উদাহরণে তুলে ধরে।
২ · সাধারণ বটলনেক ক্যাটাগরি
ভারী গণনা (জটিল লুপ, বড় ডেটাসেট সর্ট/প্রসেস করা) প্রসেসরকে ব্যস্ত রাখে — সমাধান: অ্যালগরিদম অপটিমাইজ করা বা কাজ ভাগ করে নেওয়া।
ফাইল পড়া/লেখা বা ডিস্ক অ্যাক্সেসে সময় নষ্ট হয় — প্রসেসর অলস থাকে, ডিস্কের জন্য অপেক্ষা করে।
অদক্ষ কোয়েরি, মিসিং ইনডেক্স, বা N+1 কোয়েরি সমস্যা — অনেক বাস্তব ওয়েব অ্যাপের সবচেয়ে সাধারণ বটলনেক।
বাহ্যিক API কল বা মাইক্রোসার্ভিসের মধ্যে যোগাযোগের লেটেন্সি — বিশেষ করে অনেকগুলো সিরিয়াল (একটির পর একটি) কল থাকলে দ্রুত জমে ওঠে।
৩ · একটি বাস্তব প্রোফাইলিং ডেমো
নিচের কোড সেলে একটি সিন্থেটিক পাইপলাইনের তিনটি ধাপ — একই ৬০০টি রেকর্ডের উপর কাজ করে — time.perf_counter()
দিয়ে আলাদাভাবে টাইম করা হয়েছে। প্রথম দুটি ধাপ ($O(n)$ — একবার প্রতিটি রেকর্ডের উপর দিয়ে যায়) আর তৃতীয়টি
($O(n^2)$ — প্রতিটি রেকর্ডকে বাকি সব রেকর্ডের সাথে তুলনা করে) — একই ইনপুট সাইজ নিয়েও সম্পূর্ণ ভিন্ন সংখ্যক
অপারেশন করে (৬০০ বনাম ৩,৬০,০০০ পর্যন্ত)। এই অনুপাত এতটাই বিশাল যে ফলাফলের ক্রম (কোনটি
সবচেয়ে ধীর) যেকোনো মেশিনেই একই থাকবে — যদিও প্রকৃত মিলিসেকেন্ডের মান মেশিন-ভেদে ভিন্ন হবে।
import time
import math
def fetch_records(n):
# O(n): সিমুলেটেড রেকর্ড fetch — প্রতিটি রেকর্ডের জন্য একটি সাধারণ টুপল তৈরি
records = []
for i in range(n):
records.append((i, (i * 7) % 997))
return records
def transform_records(records):
# O(n): প্রতিটি রেকর্ডে একটি হালকা গাণিতিক রূপান্তর প্রয়োগ
transformed = []
for rec_id, value in records:
transformed.append((rec_id, math.sqrt(value + 1)))
return transformed
def validate_records(records):
# O(n^2): প্রতিটি রেকর্ডকে বাকি সব রেকর্ডের সাথে তুলনা করে ডুপ্লিকেট-চেক (naive pairwise validation)
duplicates = 0
n = len(records)
for i in range(n):
for j in range(n):
if i != j and records[i][1] == records[j][1]:
duplicates += 1
return duplicates
N = 600
durations = {}
start = time.perf_counter()
records = fetch_records(N)
durations["fetch_records (O(n))"] = time.perf_counter() - start
start = time.perf_counter()
transformed = transform_records(records)
durations["transform_records (O(n))"] = time.perf_counter() - start
start = time.perf_counter()
duplicates_found = validate_records(records)
durations["validate_records (O(n^2))"] = time.perf_counter() - start
print(f"রেকর্ড সংখ্যা: {N}, পাওয়া ডুপ্লিকেট: {duplicates_found}\n")
for name, seconds in sorted(durations.items(), key=lambda kv: kv[1], reverse=True):
print(f"{name}: {seconds * 1000:.2f} ms")
bottleneck = max(durations, key=durations.get)
print(f"\nবটলনেক ধাপ: {bottleneck.strip()}")
validate_records ধাপ বাকি দুটোর তুলনায় লক্ষণীয়ভাবে বেশি সময় নেবে, কারণ এটি
$O(n^2)$ (৬০০ × ৬০০ = ৩,৬০,০০০ পর্যন্ত তুলনা করে), অথচ বাকি দুটো ধাপ $O(n)$ (মাত্র ৬০০টি করে অপারেশন) —
একই ইনপুট সাইজে প্রায় ৬০০ গুণ বেশি কাজ, যা যেকোনো মেশিনেই পরিমাপযোগ্যভাবে ধরা পড়বে। লক্ষ্য করুন
duplicates_found-এর মান ০ হওয়া উচিত — যেহেতু ৭ এবং ৯৯৭ (একটি মৌলিক সংখ্যা) পরস্পর সহমৌলিক,
তাই i ০ থেকে ৫৯৯ পর্যন্ত প্রতিটি মান আলাদা value তৈরি করে, কোনো সংঘর্ষ ছাড়াই — কিন্তু
সেটি নিশ্চিত হতে validate_records-কে তবুও সবগুলো জোড়া পরীক্ষা করতেই হয়েছে।
বটলনেক খুঁজতে অনুমান নয়, পরিমাপ লাগে। আর একবার পরিমাপ করা হলে, প্রায়ই দেখা যায় সমস্যাটি "যে কোডটুকু জটিল দেখতে" তাতে নয়, বরং যেখানে অ্যালগরিদমিক জটিলতা ইনপুট সাইজের সাথে দ্রুত বেড়ে যায় সেখানে। L35-এ এই ধরনের পরিমাপ থেকে পাওয়া থ্রুপুট সংখ্যা ব্যবহার করে বাস্তব ক্যাপাসিটি প্ল্যানিং করা শেখানো হবে।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১
কোড সেলে validate_records() ধাপ কেন সবচেয়ে বেশি সময় নেয়, যদিও তিনটি ধাপই একই ৬০০টি
রেকর্ড নিয়ে কাজ করছে?
কারণ এটি $O(n^2)$ — প্রতিটি রেকর্ডকে বাকি সব রেকর্ডের সাথে তুলনা করে, অর্থাৎ সর্বোচ্চ ৬০০ × ৬০০ = ৩,৬০,০০০টি তুলনা করতে পারে। বাকি দুটো ধাপ ($O(n)$) প্রতিটি রেকর্ডে একবার মাত্র কাজ করে — মাত্র ৬০০টি অপারেশন। একই ইনপুট সাইজ হলেও, অ্যালগরিদমের গঠন সম্পূর্ণ ভিন্ন পরিমাণ কাজ তৈরি করে।
প্র ০২ বাস্তব জীবনে "পরিমাপ করুন, অনুমান করবেন না" নীতিটি কেন এত গুরুত্বপূর্ণ?
কারণ ডেভেলপারের অনুমান প্রায়ই ভুল হয় — যে কোডটুকু "জটিল" বা "সন্দেহজনক" মনে হয়, সেটি বাস্তবে সবচেয়ে বেশি সময় না-ও নিতে পারে। প্রকৃত পরিমাপ (প্রোফাইলিং) ছাড়া অপ্টিমাইজেশনের প্রচেষ্টা ভুল জায়গায় ব্যয় হতে পারে — একটি অংশ ৫০% দ্রুত করেও যদি সেটি মোট সময়ের মাত্র ২% হয়, সামগ্রিক প্রভাব প্রায় শূন্য।
প্র ০৩
কোড সেলের duplicates_found ভ্যারিয়েবলের মান কী হওয়া উচিত এবং কেন?
০। প্রতিটি value গণনা করা হয় (i * 7) % 997 দিয়ে, যেখানে i
০ থেকে ৫৯৯ পর্যন্ত যায়। ৯৯৭ একটি মৌলিক সংখ্যা এবং ৭ এর সাথে সহমৌলিক, তাই এই ম্যাপিং ০ থেকে ৯৯৬ পরিসরে
এক-এক (injective) — অর্থাৎ ৬০০টি ভিন্ন i-এর জন্য ৬০০টি ভিন্ন মান তৈরি হয়, কোনো সংঘর্ষ ছাড়াই।
তবে এটি নিশ্চিত করতে validate_records-কে তবুও পুরো $O(n^2)$ তুলনা চালাতেই হয়েছে।
অনুশীলন
-
চিন্তা করুন: যদি
N৬০০ থেকে দ্বিগুণ করে ১২০০ করা হয়,fetch_recordsওtransform_records-এর সময় আনুমানিক কত গুণ বাড়বে, আরvalidate_records-এর সময় কত গুণ বাড়বে?$O(n)$ ধাপ দুটোর সময় আনুমানিক ২ গুণ বাড়বে (n দ্বিগুণ হলে কাজও দ্বিগুণ)। কিন্তু $O(n^2)$ ধাপের সময় আনুমানিক ৪ গুণ বাড়বে, কারণ $n$ দ্বিগুণ হলে $n^2$ চারগুণ হয় ($1200^2 = 4 \times 600^2$)। এটিই দেখায় কেন $O(n^2)$ বটলনেক ডেটা বাড়ার সাথে সাথে অসামঞ্জস্যভাবে আরও খারাপ হয়ে ওঠে।
-
পরীক্ষা করুন: কোড সেলে
N = 600-কেN = 1500-এ পরিবর্তন করে Run চাপুন —validate_records-এর সময়ের অনুপাত (বাকি দুটো ধাপের তুলনায়) আরও বাড়ে কি না লক্ষ করুন।হ্যাঁ, বাড়া উচিত। $N=1500$-এ
validate_recordsসর্বোচ্চ $1500 \times 1500 = 22,50,000$টি তুলনা করে, যেখানে অন্য দুটো ধাপ মিলিয়ে মাত্র $1500 \times 2 = 3000$টি অপারেশন করে — $N=600$-এর তুলনায় এই অনুপাত আরও বেশি অসামঞ্জস্যপূর্ণ হয়ে ওঠে, তাইvalidate_records-এর আপেক্ষিক প্রাধান্য (মোট সময়ের কত শতাংশ এটি দখল করে) আরও বৃদ্ধি পাবে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পরের পাঠ L35 পারফরম্যান্স টেস্ট ফলাফল থেকে ক্যাপাসিটি প্ল্যানিং — মাপা থ্রুপুট দিয়ে কতগুলো ইনস্ট্যান্স লাগবে তা গণনা।
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৭টি পাঠ টেস্ট ডিজাইন টেকনিক, ইউনিট টেস্টিং, টেস্ট ডাবলস, ইন্টিগ্রেশন টেস্টিং, কভারেজ, অটোমেশন, পারফরম্যান্স ও সিকিউরিটি টেস্টিং, অ্যাডভান্সড টেকনিক ও CI/CD ইন্টিগ্রেশন — বাকি পাঠগুলো শীঘ্রই যুক্ত হবে।
- সব Courses দেখুন ABCL TECH C, C++, Python, Java, JavaScript, DSA, DBMS, Discrete Mathematics, System Design, Cybersecurity, Cloud Computing & DevOps, Computer Networks, Operating Systems, Computer Architecture, Programming Languages & Compiler Design, Software Engineering & Git, Theory of Computation, Engineering Economics, Full-Stack Web Frameworks, Mobile App Development, Ethics in Computing & AI Safety ও Software Testing & Quality Assurance — সব এক জায়গায়।