Profile Code with cProfile
Profile code with cProfile — Applied AI engineering.
Focus: profile code with cprofile
Your AI pipeline runs, but it takes 40 seconds to process a batch that should take 5. You've tried adding more GPUs and caching, but nothing helps. The culprit is usually not the model inference or the network — it's the invisible, slow Python code running between API calls: string parsing, list comprehensions, data transformations. You can't fix what you can't measure. In this lesson, you'll learn to profile code with cProfile — the built-in Python profiler that tells you exactly which functions eat your CPU time — so you can stop guessing and start optimizing where it matters.
The problem this lesson solves
In applied AI engineering, performance problems hide in plain sight. A typical pipeline might call an LLM API, parse the response, transform data, and store results. When it's slow, the natural impulse is to blame the model or the network. But often, the bottleneck is a function you wrote — a naive loop, an inefficient data structure, or a regex that runs a million times.
Without profiling, you're flying blind. Optimizing the wrong code is worse than not optimizing at all: you waste hours and deliver zero improvement. Profile code with cProfile to get a precise, function-level breakdown of execution time. With cProfile, you can:
- Find the top 10 functions that consume CPU.
- Identify whether something is CPU-bound or I/O-bound.
- Compare two implementations to see which is truly faster.
This is not speculative advice — it's the first step in any serious performance engineering loop, and it's built into the Python standard library, so no extra dependencies are needed.
Core concept / mental model
Think of cProfile as a stopwatch for every function call. When you run your script with cProfile, it records:
- ncalls — how many times a function was called.
- tottime — total time spent in the function itself (excluding subcalls).
- cumtime — total time spent in the function and all its subcalls.
- percall — time per call.
A useful mental model is a restaurant kitchen. The cumtime is the time a dish spends from order to plating — including waiting for the oven. The tottime is the time the chef actively spends chopping vegetables. If you want to speed up the kitchen, you focus on high-cumtime dishes (the bottleneck), then drill into which chef (function) is responsible.
In Python, there are two levels of profiling:
- cProfile — deterministic profiler, measures every call, suitable for CPU-bound code.
- profile — pure-Python profiler, slower but more detailed.
- timeit — micro-benchmark for small snippets, not for whole programs.
For AI workloads, you'll almost always reach for cProfile because it has low overhead and gives you the big picture.
How it works step by step
Profiling with cProfile follows a simple rhythm: run, inspect, optimize, re-run.
- Annotate your code — Add
import cProfile; cProfile.run('your_function()')or run from the command line. - Generate a stats file — Save the profiler output to a file for deeper analysis.
- Read the report — Use
pstatsto sort and filter the results. - Identify the bottleneck — Look for high
tottimeorcumtimefunctions. - Optimize the hot spot — Replace the slow implementation with something faster.
- Re-profile — Confirm the improvement; don't guess.
The key is to profile before optimizing. The step-by-step flow is:
# Run your script with cProfile from the command line
python -m cProfile -o output.prof my_script.py
This writes the stats to output.prof, which you can then analyze with pstats.
Hands-on walkthrough
Let's work through a realistic example: you have a function that filters and transforms a list of dictionaries — common when preprocessing AI model inputs.
Step 1: Write a slow script
Create slow_pipeline.py with a function that simulates a sluggish data transformation.
import time
def transform_data(items):
result = []
for item in items:
processed = []
for key in item:
value = item[key]
if value > 0:
processed.append((key, value * 2))
if processed:
result.append(processed)
time.sleep(0.001) # Simulate I/O or CPU work
return result
def main():
data = [{chr(97 + i): i for i in range(20)} for _ in range(1000)]
transformed = transform_data(data)
print(f"Transformed {len(transformed)} batches")
if __name__ == "__main__":
main()
Step 2: Profile it with cProfile
Run the script under cProfile and generate a stats file:
python -m cProfile -o pipeline.prof slow_pipeline.py
To see the output in the terminal (simpler but less flexible), you can also just run it directly.
Step 3: Analyze the stats with pstats
Now open a Python REPL and load the stats:
import pstats
from pstats import SortKey
p = pstats.Stats('pipeline.prof')
p.strip_dirs().sort_stats(SortKey.TIME).print_stats(10)
Expected output (trimmed for brevity):
ncalls tottime percall cumtime percall filename:lineno(function)
1000 0.002 0.000 1.010 0.001 slow_pipeline.py:4(transform_data)
1000 1.005 0.001 1.005 0.001 {built-in method time.sleep}
Notice that time.sleep dominates the tottime — that's your bottleneck. In your real code, this might be a regex or a loop, but the same idea applies.
Step 4: Optimize and re-profile
Assuming the sleep was artificial, let's remove it and replace the inner loop with a list comprehension:
import time
def transform_data(items):
result = []
for item in items:
processed = [(key, value * 2) for key, value in item.items() if value > 0]
if processed:
result.append(processed)
# No sleep now — or replace with actual I/O
return result
def main():
data = [{chr(97 + i): i for i in range(20)} for _ in range(1000)]
transformed = transform_data(data)
print(f"Transformed {len(transformed)} batches")
if __name__ == "__main__":
main()
Run cProfile again and compare the tottime values. You should see a dramatic drop in time spent in transform_data.
Compare options / when to choose what
| cProfile | profile | timeit |
|---|---|---|
| Built-in, low overhead | Pure Python, slower | Micro-benchmark only |
| Whole program profiling | Whole program, more detail | Small code snippets |
| C extension, fast | Inspectable, great for debugging | Best for comparing tiny alternatives |
| Use for AI pipelines | Use when cProfile misses details | Use for loop vs. comprehension tests |
For AI engineering, cProfile is your default. Use profile if you need to inspect callers/callees in detail, and timeit when you're deciding between two one-liner approaches.
Pro tip: For profiling memory, use
tracemallocin parallel with cProfile — CPU time isn't the only resource you care about.
Troubleshooting & edge cases
- cProfile shows nothing for async code — cProfile doesn't track
asynciotasks well. For async, usepy-spyor manually log aroundawaitpoints. - Stats file is empty — If your script calls
sys.exit()early or imports fail, profiling may not run. Ensure your main function actually executes. - Output too noisy — Sort by
CUMULATIVEto see whole call chains, not just leaf functions. - cProfile slows your script drastically — For performance-critical test runs, use
profilewith sampling, or increase the number of repetitions to get stable numbers. - Built-in functions dominate — If you see
{built-in method builtins.len}high, it's a symptom of massive list comprehensions — consider vectorizing with NumPy.
What you learned & what's next
You now know how to profile code with cProfile to identify bottlenecks in your AI pipelines. You can:
- Run cProfile from the command line or programmatically.
- Interpret
tottime,cumtime, andncalls. - Sort and filter stats with
pstats. - Re-profile after optimizations to prove improvements.
Remember: profiling is a skill you'll use every time your model is fast but your pipeline is slow. In the next lesson, you'll learn memory profiling with tracemalloc — because CPU time is only half the story. You'll apply these same profiling concepts to track memory leaks and reduce RAM usage in long-running AI services.
Keep this lesson fresh: whenever you feel the urge to prematurely optimize, profile first. Your future self will thank you.
Practice recap
Create a small script that parses a large JSON file and runs a few string operations. Profile it with cProfile, identify the top three functions, and replace the slowest one with a list comprehension. Re-profile and compare the tottime — you should see at least a 2x speedup. This exercise will cement the profile-measure-optimize cycle.
Common mistakes
- Optimizing before profiling — you might spend hours on a function that's not the bottleneck.
- Using cumtime alone without checking tottime — you'll chase subcalls instead of the real hot spot.
- Forgetting to re-run cProfile after optimizations — you can't know if you improved anything without measuring again.
- Ignoring I/O-bound functions — cProfile shows CPU time, but sleeps and network waits show up as high tottime; treat them separately.
Variations
- Use py-spy to profile running processes without interrupting them — great for production services.
- Profile memory with tracemalloc or memory_profiler when CPU isn't the issue.
- For micro-benchmarks, use timeit to compare small alternatives before committing to a design.
Real-world use cases
- Find the slowest preprocessing step in a batch inference pipeline for image classification.
- Identify a regex-heavy parser that slows down structured data extraction from LLM outputs.
- Compare two candidate implementations of a feature-engineering function before deploying to production.
Key takeaways
- cProfile is built-in and perfect for CPU-bound profiling — no external packages needed.
- Understand the difference between tottime (own time) and cumtime (with subcalls) to find true bottlenecks.
- Profile first, optimize second — always re-profile to confirm improvement.
- Use pstats to sort by TIME or CUMULATIVE to focus on the most expensive functions.
- Remember that cProfile doesn't capture async tasks — use alternative tools for those.
- Profiling is a continuous loop: run, inspect, optimize, re-run — make it part of your workflow.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.