Python Performance
Name the metric first: wall time, CPU time, allocation count, retained heap,
peak RSS, or I/O wait. Pin Python, dependencies, input, and environment; then
change one profiled cause.
Measurement
- Use
pyperf for repeatable benchmarks with calibration, worker processes,
metadata, and statistical comparison.
- Use
timeit only for small fragments. It disables cyclic GC during timing
unless explicitly re-enabled, which can make allocation-heavy code look
unlike production.
- Use
cProfile for call counts and cumulative development profiles; use a
sampling profiler such as py-spy for lower-overhead process observation.
- Use
tracemalloc for Python-managed allocations. If RSS grows while its
traces remain stable, inspect native allocations or fragmentation with
Memray or an OS profiler.
sys.getsizeof is shallow; it does not measure referenced objects.
- Use
python -X importtime before changing startup imports.
Data structures and Python operations
Choose from access patterns:
| Need |
Prefer |
| Membership or deduplication |
set or dict, not repeated list scans |
| Queue operations at both ends |
collections.deque, not list.pop(0) |
| Priority queue |
heapq |
| Search in maintained sorted data |
bisect |
| Mutable binary accumulation |
bytearray, then bytes(buffer) |
| Many string fragments |
collect fragments and "".join(parts) |
These choices change semantics and memory. Do not replace a list when callers
need indexing, slicing, or compact iteration.
Generators avoid eager materialization but add iteration overhead and cannot be
reused. Built-ins and comprehensions often move work into optimized C loops,
but they are not automatically faster for every workload.
lru_cache trades CPU for retained memory and invalidation. On an instance
method, cache keys retain self; avoid it when instances must be collected.
@dataclass(slots=True) or __slots__ can reduce memory for many instances but
affects dynamic attributes, inheritance, weak references, serialization, and
framework integration.
Memory and GC
CPython uses reference counting plus cyclic GC. Distinguish:
- growing Python allocation traces;
- retained reachable objects;
- native allocations;
- allocator fragmentation;
- peak RSS;
- allocation churn that increases CPU without retaining memory.
High RSS alone is not a leak. Tune GC thresholds, call gc.freeze(), or change
allocators only after pause, allocation, or copy-on-write measurements identify
the collector or allocator as the cause. GC defaults differ by Python version
and free-threaded build.
Threads, asyncio, and processes
- Threads overlap many blocking I/O operations because those calls release the
GIL. Pure-Python CPU threads do not execute bytecode in parallel under the
normal GIL; native extensions may release it.
asyncio is cooperative concurrency. Any blocking call or long CPU loop in a
coroutine stalls the event loop. Use bounded queues when producers can outrun
consumers, and preserve cancellation and shutdown.
- Processes provide CPU parallelism but add startup, pickling, IPC, memory, and
failure handling. Include all of those in the benchmark.
- Start methods vary by platform and Python version. Libraries should not force
a global method without owning application lifecycle.
- Free-threaded CPython enables parallel Python threads but adds evolving
overhead, synchronization requirements, and extension compatibility. Test the
exact interpreter and dependency set.
I/O and services
- Use buffering for repeated small reads and writes.
readinto() can reuse a
buffer in measured binary pipelines but adds ownership complexity.
- Batch database and network operations to reduce round trips. Oversized
batches increase memory, lock duration, tail latency, and retry scope.
- Avoid constructing expensive log messages when the level is disabled. Queue
handlers move slow output off latency-sensitive threads but require bounded
capacity, ordering, loss, and shutdown decisions.
- Preserve flush, EOF, error, retry, ordering, cancellation, and protocol
behavior when optimizing I/O.
NumPy and native acceleration
- Chained NumPy operations can allocate full-size temporaries. Use
out=,
in-place operations, chunking, or fused kernels only after CPU and memory
profiles show the temporary matters.
- Check contiguity and strides when native kernels copy or traverse arrays
poorly. Normalize layout once at a boundary, not repeatedly in a loop.
- BLAS, process pools, application threads, and runtimes can each create worker
pools. Measure oversubscription before limiting them with
threadpoolctl or
environment settings.
- Warm Numba before benchmarking. It helps supported Python-loop work, not code
already dominated by optimized NumPy kernels.
- Cython, PyO3/Rust, GPU code, and alternative runtimes add compilation,
transfer, ABI, packaging, debugging, and maintenance. Batch enough work per
boundary crossing to justify them and keep a tested Python path when useful.
CPython specialization
Use dis.dis(fn, adaptive=True) after warm-up as supporting evidence for a hot
loop. Do not redesign APIs to preserve one specialized opcode; specialization
rules change between versions. Re-measure after Python upgrades.
Regression guards
Use narrow allocation, output-size, startup, or memory guards when the
toolchain and platform are pinned. Wall-time gates require dedicated hardware
or enough margin to avoid flaking; keep shared-runner timing advisory. Never
compare runs with different GC modes, profilers, hooks, or calibration.
Related skills
code-review checks a completed diff. When invoked from code-review, do not
invoke it again.
code-simplifier runs after the gain is proven.
change-impact-auditor traces environment, serialization, imports, logging,
and concurrency changes.
runtime-process-debugging owns subprocess, pipe, lifecycle, and shutdown
failures.
Correctness overrides performance.
1---2name: python-performance3description: Profile and optimize Python CPU, memory, I/O, concurrency, and numerical performance.4---56# Python Performance78Name the metric first: wall time, CPU time, allocation count, retained heap,9peak RSS, or I/O wait. Pin Python, dependencies, input, and environment; then10change one profiled cause.1112## Measurement1314- Use `pyperf` for repeatable benchmarks with calibration, worker processes,15 metadata, and statistical comparison.16- Use `timeit` only for small fragments. It disables cyclic GC during timing17 unless explicitly re-enabled, which can make allocation-heavy code look18 unlike production.19- Use `cProfile` for call counts and cumulative development profiles; use a20 sampling profiler such as `py-spy` for lower-overhead process observation.21- Use `tracemalloc` for Python-managed allocations. If RSS grows while its22 traces remain stable, inspect native allocations or fragmentation with23 Memray or an OS profiler.24- `sys.getsizeof` is shallow; it does not measure referenced objects.25- Use `python -X importtime` before changing startup imports.2627## Data structures and Python operations2829Choose from access patterns:3031| Need | Prefer |32|---|---|33| Membership or deduplication | `set` or `dict`, not repeated list scans |34| Queue operations at both ends | `collections.deque`, not `list.pop(0)` |35| Priority queue | `heapq` |36| Search in maintained sorted data | `bisect` |37| Mutable binary accumulation | `bytearray`, then `bytes(buffer)` |38| Many string fragments | collect fragments and `"".join(parts)` |3940These choices change semantics and memory. Do not replace a list when callers41need indexing, slicing, or compact iteration.4243Generators avoid eager materialization but add iteration overhead and cannot be44reused. Built-ins and comprehensions often move work into optimized C loops,45but they are not automatically faster for every workload.4647`lru_cache` trades CPU for retained memory and invalidation. On an instance48method, cache keys retain `self`; avoid it when instances must be collected.49`@dataclass(slots=True)` or `__slots__` can reduce memory for many instances but50affects dynamic attributes, inheritance, weak references, serialization, and51framework integration.5253## Memory and GC5455CPython uses reference counting plus cyclic GC. Distinguish:5657- growing Python allocation traces;58- retained reachable objects;59- native allocations;60- allocator fragmentation;61- peak RSS;62- allocation churn that increases CPU without retaining memory.6364High RSS alone is not a leak. Tune GC thresholds, call `gc.freeze()`, or change65allocators only after pause, allocation, or copy-on-write measurements identify66the collector or allocator as the cause. GC defaults differ by Python version67and free-threaded build.6869## Threads, asyncio, and processes7071- Threads overlap many blocking I/O operations because those calls release the72 GIL. Pure-Python CPU threads do not execute bytecode in parallel under the73 normal GIL; native extensions may release it.74- `asyncio` is cooperative concurrency. Any blocking call or long CPU loop in a75 coroutine stalls the event loop. Use bounded queues when producers can outrun76 consumers, and preserve cancellation and shutdown.77- Processes provide CPU parallelism but add startup, pickling, IPC, memory, and78 failure handling. Include all of those in the benchmark.79- Start methods vary by platform and Python version. Libraries should not force80 a global method without owning application lifecycle.81- Free-threaded CPython enables parallel Python threads but adds evolving82 overhead, synchronization requirements, and extension compatibility. Test the83 exact interpreter and dependency set.8485## I/O and services8687- Use buffering for repeated small reads and writes. `readinto()` can reuse a88 buffer in measured binary pipelines but adds ownership complexity.89- Batch database and network operations to reduce round trips. Oversized90 batches increase memory, lock duration, tail latency, and retry scope.91- Avoid constructing expensive log messages when the level is disabled. Queue92 handlers move slow output off latency-sensitive threads but require bounded93 capacity, ordering, loss, and shutdown decisions.94- Preserve flush, EOF, error, retry, ordering, cancellation, and protocol95 behavior when optimizing I/O.9697## NumPy and native acceleration9899- Chained NumPy operations can allocate full-size temporaries. Use `out=`,100 in-place operations, chunking, or fused kernels only after CPU and memory101 profiles show the temporary matters.102- Check contiguity and strides when native kernels copy or traverse arrays103 poorly. Normalize layout once at a boundary, not repeatedly in a loop.104- BLAS, process pools, application threads, and runtimes can each create worker105 pools. Measure oversubscription before limiting them with `threadpoolctl` or106 environment settings.107- Warm Numba before benchmarking. It helps supported Python-loop work, not code108 already dominated by optimized NumPy kernels.109- Cython, PyO3/Rust, GPU code, and alternative runtimes add compilation,110 transfer, ABI, packaging, debugging, and maintenance. Batch enough work per111 boundary crossing to justify them and keep a tested Python path when useful.112113## CPython specialization114115Use `dis.dis(fn, adaptive=True)` after warm-up as supporting evidence for a hot116loop. Do not redesign APIs to preserve one specialized opcode; specialization117rules change between versions. Re-measure after Python upgrades.118119## Regression guards120121Use narrow allocation, output-size, startup, or memory guards when the122toolchain and platform are pinned. Wall-time gates require dedicated hardware123or enough margin to avoid flaking; keep shared-runner timing advisory. Never124compare runs with different GC modes, profilers, hooks, or calibration.125126## Related skills127128- `code-review` checks a completed diff. When invoked from `code-review`, do not129 invoke it again.130- `code-simplifier` runs after the gain is proven.131- `change-impact-auditor` traces environment, serialization, imports, logging,132 and concurrency changes.133- `runtime-process-debugging` owns subprocess, pipe, lifecycle, and shutdown134 failures.135136Correctness overrides performance.