"It feels slow" is not a profile. Measure before you optimize — intuition about hot paths is wrong more often than engineers like to admit.
Measure first
Profiling exists because human intuition about where a program spends its time is unreliable. The
90/10 rule (roughly 90% of runtime lives in 10% of the code) means that optimizing the wrong function
costs real time for zero benefit. Always profile before optimizing, and re-profile after — the
hot path often moves once the biggest bottleneck is fixed.
time <command> || the simplest profiler there is: wall-clock, user, and system CPU time for a whole command | 'time1'
perf (Linux)
perf is the standard Linux profiler — it samples the program's call stack at a fixed frequency using
hardware performance counters, with very low overhead.
perf stat <command> || runs <command> and prints high-level counters afterward: instructions, cycles, cache misses, branch mispredictions | 'perf2'
perf record -g <command> || samples <command>'s call stack (-g) while it runs, saving to perf.data | 'perf3'
perf report || shows an interactive, function-by-function breakdown of the last perf record | 'perf4'
perf top || live, continuously-updating view of which functions are hottest right now, system-wide | 'perf5'
Python: cProfile & py-spy
python3 -m cProfile <script.py> || profiles a script and prints per-function call counts and time, built into the standard library | 'py1'
python3 -m cProfile -o <out.prof> <script.py> || same, but saves the results to a file for later analysis instead of printing | 'py2'
python3 -m pstats <out.prof> || opens a saved profile in an interactive browser: sort by time, see callers/callees | 'py3'
cProfile instruments every function call, which is precise but adds real overhead and can distort timing.
py-spy instead samples a running process from the outside — no code changes, works on processes
you didn't even start with profiling in mind, and safe to attach to production.
pip install py-spy || installs py-spy | 'py4'
py-spy top --pid <pid> || live, top-like view of a running Python process's hottest functions, no restart needed | 'py5'
py-spy record -o <profile.svg> --pid <pid> || records a flame graph of a running process | 'py6'
Memory profiling
/usr/bin/time -v <command> || reports peak memory usage (maximum resident set size) for a command, among other stats | 'mem1'
pip install memory-profiler || installs a line-by-line memory profiler for Python | 'mem2'
python3 -m memory_profiler <script.py> || shows memory usage for each line of a decorated function | 'mem3'
valgrind --tool=massif ./<binary> || profiles heap memory usage over time for a C/C++ binary | 'mem4'
Reading a flame graph
A flame graph turns thousands of stack samples into one picture. Each box is a function; the box's
width is proportional to how often it appeared in a sample (i.e. how much total time it or its
children consumed) — width is what to look at, not the x-position (which usually has no
chronological meaning). Stacking (a box sitting on top of another) shows the call relationship:
the box on top was called by the box below it. Wide, flat plateaus near the top of the stack are exactly
the "10% of code, 90% of time" functions worth optimizing first.
related topics
GPU Optimization — GPU-specific profiling tools like Nsight and rocprof. Python Optimization — what to actually do with what a profiler tells you.