Performance: Profiling & Optimization
Python প্রোগ্রামকে দ্রুত করা — আগে মাপুন, পরে অপ্টিমাইজ করুন
1. The First Rule of Optimization
Donald Knuth famously said, "Premature optimization is the root of all evil." The full quote continues: "Yet we should not pass up our opportunities in that critical 3%." In practice that means: measure first, then optimize only the slow 3% that actually matters. Guessing where a program is slow is almost always wrong.
In this module you'll learn the Python tools that turn guessing into measuring:
timeit, time.perf_counter, cProfile and tracemalloc.
Then we'll apply a four-step optimization playbook on real examples.
2. Measuring Time — timeit and perf_counter
Python provides two reliable ways to measure wall-clock time: the timeit module for
short, repeatable snippets, and time.perf_counter() for longer, one-off measurements.
Never use time.time() for benchmarks — it is wall-clock and can be adjusted by NTP.
timeit মডিউল এবং দীর্ঘ one-off পরিমাপের জন্য time.perf_counter()। Benchmark-এ time.time() কখনো ব্যবহার করবেন না — এটি wall-clock, NTP দ্বারা সমন্বিত হতে পারে।
import timeit, time
# 1) timeit — repeats the snippet many times, returns total seconds
t = timeit.timeit("sum(range(10_000))", number=1000)
print(f"sum(range): {t*1000:.2f} ms for 1000 runs")
# 2) perf_counter — high-resolution monotonic clock
start = time.perf_counter()
total = 0
for i in range(10_000):
total += i
elapsed = time.perf_counter() - start
print(f"manual loop: {elapsed*1000:.3f} ms")
3. Finding Hotspots — cProfile
cProfile measures how many times each function was called and how long it took.
You get a breakdown sorted by cumulative or total time, which instantly reveals the 3%.
cProfile প্রতিটি ফাংশন কতবার ডাকা হলো এবং কত সময় নিল তার হিসাব দেয়। cumulative বা total সময় অনুযায়ী sort করলেই সেই ৩% hotspot বেরিয়ে আসে।
import cProfile, pstats, io
def slow_sum(n):
total = 0
for i in range(n):
total += i * i
return total
def workload():
for _ in range(5):
slow_sum(100_000)
pr = cProfile.Profile()
pr.enable()
workload()
pr.disable()
buf = io.StringIO()
pstats.Stats(pr, stream=buf).sort_stats("cumulative").print_stats(5)
print(buf.getvalue())
4. The Biggest Win Is Almost Always Algorithmic
A smarter algorithm beats micro-optimization every time. Below, replacing repeated list membership
(O(n) per lookup) with set membership (O(1)) turns a quadratic loop into a linear one.
in চেক O(n), কিন্তু set-এ in চেক O(1) — এটি quadratic loop-কে linear করে দেয়।
import time
data = list(range(20_000))
lookup = list(range(0, 20_000, 2))
# Slow: list membership — O(n) each time
t0 = time.perf_counter()
hits = sum(1 for x in data if x in lookup)
print(f"list: {(time.perf_counter()-t0)*1000:.1f} ms hits={hits}")
# Fast: set membership — O(1) each time
lookup_set = set(lookup)
t0 = time.perf_counter()
hits = sum(1 for x in data if x in lookup_set)
print(f"set: {(time.perf_counter()-t0)*1000:.1f} ms hits={hits}")
5. Built-ins, Comprehensions and functools.lru_cache
Python's built-ins (sum, map, any) are implemented in C and usually
beat a hand-written loop. For pure functions with repeated arguments, @lru_cache provides
automatic memoization.
sum, map) C-তে লেখা, হাত দিয়ে লেখা loop-এর চেয়ে দ্রুত। একই argument বারবার এলে @lru_cache দিয়ে স্বয়ংক্রিয় memoization করা যায়।
from functools import lru_cache
import time
def fib_slow(n):
if n < 2: return n
return fib_slow(n-1) + fib_slow(n-2)
@lru_cache(maxsize=None)
def fib_fast(n):
if n < 2: return n
return fib_fast(n-1) + fib_fast(n-2)
t0 = time.perf_counter(); fib_slow(30); slow = time.perf_counter()-t0
t0 = time.perf_counter(); fib_fast(30); fast = time.perf_counter()-t0
print(f"slow {slow*1000:.1f} ms fast {fast*1000:.3f} ms")
print(f"speedup: {slow/fast:.0f}x")
6. Memory Profiling with tracemalloc
CPU is not the only bottleneck. tracemalloc tracks peak memory and shows the exact source
lines that allocated it.
tracemalloc peak memory ট্র্যাক করে এবং কোন লাইন সেটি allocate করেছে তা দেখায়।
import tracemalloc
tracemalloc.start()
big = [i*i for i in range(200_000)]
current, peak = tracemalloc.get_traced_memory()
print(f"current={current/1024:.0f} KiB peak={peak/1024:.0f} KiB")
tracemalloc.stop()
[x*x for x in range(N)] with (x*x for x in range(N)) when you only iterate once.
একবারই iterate করলে list comprehension-এর বদলে generator expression ব্যবহার করলে memory অনেক কম লাগে।
7. Vocabulary (শব্দভাণ্ডার)
| Term | Meaning | বাংলায় |
|---|---|---|
| Hotspot | The small part of code where most time is spent. | যে ছোট অংশে বেশিরভাগ সময় ব্যয় হয়। |
| Big-O | Asymptotic growth of running time w.r.t. input size. | ইনপুটের সাথে runtime কেমন বাড়ে তার মাপ। |
| Memoization | Caching function results keyed by arguments. | একই argument-এর জন্য ফলাফল cache করা। |
| cProfile | Python's deterministic profiler in the stdlib. | Python-এর built-in deterministic profiler। |
| tracemalloc | Stdlib module to trace memory allocations. | Memory allocation ট্র্যাক করার stdlib মডিউল। |
| Vectorization | Replacing loops with array operations (NumPy). | Loop-এর বদলে array operation ব্যবহার। |
8. Practice Problems
Measure before you change. Each problem asks you to time two versions and compare.
-
Use
timeitto comparesum(range(100_000))with a hand-written loop.timeitদিয়েsum(range(100_000))এবং হাতে-লেখা loop-এর সময় তুলনা করুন।✨ Show Answer (উত্তর দেখুন)
ans1.pyimport timeit a = timeit.timeit("sum(range(100_000))", number=200) b = timeit.timeit(""" t = 0 for i in range(100_000): t += i """, number=200) print(f"built-in sum : {a*1000:.1f} ms") print(f"manual loop : {b*1000:.1f} ms") -
Replace a list-based membership filter with a set-based one and measure.List-ভিত্তিক membership filter-কে set-ভিত্তিক করে সময় মাপুন।
✨ Show Answer (উত্তর দেখুন)
ans2.pyimport time banned = list(range(0, 5000, 3)) users = list(range(5000)) t = time.perf_counter() ok = [u for u in users if u not in banned] print(f"list : {(time.perf_counter()-t)*1000:.1f} ms") s = set(banned) t = time.perf_counter() ok = [u for u in users if u not in s] print(f"set : {(time.perf_counter()-t)*1000:.1f} ms") -
Memoize a recursive function of your choice with
@lru_cacheand show the speedup.একটি recursive ফাংশনকে@lru_cacheদিয়ে memoize করে speedup দেখান।✨ Show Answer (উত্তর দেখুন)
ans3.pyfrom functools import lru_cache @lru_cache(maxsize=None) def paths(r, c): if r == 0 or c == 0: return 1 return paths(r-1, c) + paths(r, c-1) print(paths(15, 15)) -
Why should you not use
time.time()for benchmarking? Answer in two sentences.Benchmark-এtime.time()কেন ব্যবহার করবেন না — দুই বাক্যে বলুন।✨ Show Answer (উত্তর দেখুন)
Answer:
time.time()returns wall-clock time which can be adjusted by the OS (NTP, DST), so a benchmark could even read negative.time.perf_counter()is monotonic and has the highest resolution the platform offers.time.time()wall-clock দেয় যা OS (NTP/DST) সমন্বয় করতে পারে — benchmark negative-ও হতে পারে।time.perf_counter()monotonic ও সর্বোচ্চ রেজোলিউশনের। -
Use
tracemallocto measure peak memory of creating a list of 500,000 ints, and compare with a generator.tracemallocদিয়ে ৫,০০,০০০ int-এর list ও generator-এর peak memory তুলনা করুন।✨ Show Answer (উত্তর দেখুন)
ans5.pyimport tracemalloc tracemalloc.start() data = [i for i in range(500_000)] _, peak_list = tracemalloc.get_traced_memory() tracemalloc.stop() tracemalloc.start() total = sum(i for i in range(500_000)) _, peak_gen = tracemalloc.get_traced_memory() tracemalloc.stop() print(f"list peak : {peak_list/1024:.0f} KiB") print(f"gen peak : {peak_gen/1024:.0f} KiB")
Summary — Module 31
Measure first with timeit, perf_counter, cProfile and tracemalloc.
The biggest wins are almost always algorithmic (O(n²) → O(n log n), list → set, recursion → memoization).
Reach for Python built-ins, comprehensions, and generators. Only after all of that should you consider C extensions,
Cython, Numba, or PyPy.