cProfile: Finding the Hotspot
cProfile: Finding the Hotspot
Start broad — measure before changing anything:
python -m cProfile -s cumtime app.py # sort by cumulative time
python -m cProfile -s tottime app.py # sort by time spent in the function itself
Or profile a callable in-process:
import cProfile, pstats
cProfile.run("solve(data)", "prof.out")
pstats.Stats("prof.out").strip_dirs().sort_stats("cumulative").print_stats(10)
Read the two time columns carefully: tottime excludes sub-calls (the place to optimize inside), cumtime includes them (the place to find a slow call chain). A function with huge call counts and small tottime still costs time through per-call overhead — that pattern points to unnecessary loops rather than expensive bodies.
Line and Memory Profilers
Line and Memory Profilers
cProfile reports per function; hot loops need per line:
pip install line_profiler memory_profiler
kernprof -l -v script.py # runs @profile functions and prints per-line timing
python -m memory_profiler script.py # line-by-line memory growth
For allocation sites rather than totals, use the stdlib tracemalloc:
import tracemalloc
tracemalloc.start()
# ... workload ...
snap = tracemalloc.take_snapshot()
for stat in snap.compare_to(tracemalloc.take_snapshot(), "lineno")[:5]:
print(stat)
The diff shows which source lines allocated the memory still alive — the right tool when RSS grows steadily or a cache never evicts.
Benchmarking Discipline and Pitfalls
Benchmarking Discipline and Pitfalls
Microbenchmarks lie easily. Rules that keep them honest:
- Use
timeit(ortime.perf_counter()) — nevertime.time(), which jumps with clock adjustments - Warm up first; discard the first run so caches and lazy imports settle
- Vary input sizes to see complexity, not one lucky sample
- Remember
cProfileadds per-call overhead — tiny functions look relatively slower under it
Interview angle: the winning narrative is measure → hypothesize → change → re-measure. State the profile evidence, apply the fix (often an algorithmic change: set membership instead of list scans), then show the before/after numbers. Micro-tweaks like local-variable binding only matter after the algorithm is right.
Quiz
1. A function has low tottime but very high cumtime. What does that suggest?
2. Which tool attributes memory growth to specific source lines?
3. Why is time.perf_counter() preferred over time.time() for benchmarks?
Flashcards
Question
tottime vs cumtime
Click to reveal answer
Answer
tottime = time in the function alone; cumtime = including everything it calls.
Question
kernprof -l -v
Click to reveal answer
Answer
Runs a script with @profile hooks from line_profiler and prints per-line timings.
Question
tracemalloc
Click to reveal answer
Answer
Stdlib allocator tracer; snapshot diffs reveal which lines hold the memory.
Question
Benchmark hygiene
Click to reveal answer
Answer
perf_counter, warm-up run, repeated samples, varying input sizes.
Revision Notes
Key Takeaways
- 1. Profile first: cProfile for the hotspot, line/memory tools to drill down
- 2. tottime vs cumtime distinguishes a slow function from a slow dependency chain
- 3. Fix algorithms before micro-optimizing, and prove wins with repeatable measurements
Interview Tips
- • Describe your optimization loop with specific tools, not just "I profiled it"
- • Bring a before/after measurement story and explain why the change was safe
Cheat Sheet
python -m cProfile -s cumtime app.py; tottime vs cumtime; kernprof -l -v; tracemalloc snapshots; timeit + perf_counter; measure → fix → re-measure.