Profile Code with cProfile

Profile code with cProfile — Applied AI engineering.

Focus: profile code with cprofile

Sponsored

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.

  1. Annotate your code — Add import cProfile; cProfile.run('your_function()') or run from the command line.
  2. Generate a stats file — Save the profiler output to a file for deeper analysis.
  3. Read the report — Use pstats to sort and filter the results.
  4. Identify the bottleneck — Look for high tottime or cumtime functions.
  5. Optimize the hot spot — Replace the slow implementation with something faster.
  6. 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 tracemalloc in 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 asyncio tasks well. For async, use py-spy or manually log around await points.
  • 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 CUMULATIVE to see whole call chains, not just leaf functions.
  • cProfile slows your script drastically — For performance-critical test runs, use profile with 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, and ncalls.
  • 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

  1. Use py-spy to profile running processes without interrupting them — great for production services.
  2. Profile memory with tracemalloc or memory_profiler when CPU isn't the issue.
  3. 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.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.