# Python Performance and Profiling

## 1. Measure Before Optimizing

| Question | Tool |
|---|---|
| How long does this snippet take? | `timeit` (`python -m timeit "..."`), `pytest-benchmark` |
| Which functions use the most time? | `cProfile` + `pstats`, `snakeviz` to visualize |
| Line-by-line hot spots? | `line_profiler` |
| Production process, no code changes? | **py-spy** (sampling profiler: `py-spy top --pid`, flame graphs) |
| Where does memory go? | `tracemalloc`, memray, scalene |

```python
import cProfile, pstats
with cProfile.Profile() as pr:
    run_report()
pstats.Stats(pr).sort_stats("cumulative").print_stats(10)
```

`tottime` is time spent inside the function itself; `cumtime` includes everything it calls.

## 2. Algorithmic Wins First

Most speedups come from doing less work:

- `x in list` is O(n); `x in set`/`dict` is O(1). Converting a list to a set before repeated membership tests can turn O(n²) into O(n).
- Avoid repeated work in loops (hoist invariant computations, precompute lookups).
- Use the right structure: `collections.deque` for queues, `heapq` for top-k, `bisect` for sorted inserts, `Counter` for counting.
- Batch I/O: one query returning 1,000 rows beats 1,000 queries; reuse HTTP sessions.

## 3. Python-Level Speedups

- Built-ins and comprehensions run in C: `sum(x*x for x in xs)` beats a manual loop; `"".join(parts)` beats `+=` in a loop.
- Local variable lookups are faster than globals/attributes in tight loops.
- **Caching**: `functools.lru_cache`/`cache` for pure functions with repeated arguments.
- **Generators** avoid building large intermediate lists.
- Newer Python versions are much faster (3.11+ gained 10–60% on many workloads).

## 4. Going Native or Parallel

- **NumPy/pandas/polars** vectorize numeric work in C/Rust.
- **Cython, mypyc, Rust (PyO3)** for hot loops that can't be vectorized.
- **Multiprocessing** for CPU-bound parallel work (the GIL), **asyncio** for I/O concurrency.
- **PyPy** (JIT) can speed up pure-Python code dramatically, if your dependencies support it.

## 5. Web-Service Specific

Profile real requests: database query counts and N+1 problems, serialization costs (orjson), connection pooling, caching, and running enough worker processes. APM tracing (OpenTelemetry) shows where request time goes.

## Common Pitfalls

1. **Optimizing without profiling**: guessing the bottleneck.
2. **Micro-optimizing** code that runs once while a query runs 1,000 times.
3. **`timeit` on tiny inputs** that don't represent production.
4. **Unbounded `lru_cache`** growing memory forever.
5. **Threads for CPU-bound code**.

## Quick Reference

```text
timeit · cProfile/pstats (tottime vs cumtime) · line_profiler · py-spy in production · tracemalloc/memray
algorithms first: set/dict lookups, right structures, batch I/O · built-ins & comprehensions · "".join
lru_cache for pure functions · NumPy / native extensions · processes for CPU · upgrade Python
```
