--- name: python-performance description: Profile and optimize Python CPU, memory, I/O, concurrency, and numerical performance. --- # 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.