The Python single-node performance ecosystem

Objectives

  • Distinguish concurrency from parallel execution.

  • Explain how the GIL affects CPU-bound Python code.

  • Choose among threads, processes, native code, and Dask for a workload.

Instructor note

  • 20 min teaching/type-along

  • 15 min exercises and discussion

Modern nodes gain throughput mainly from multiple cores and vector units. Python does not automatically use those resources, and dividing work creates scheduling, communication, and memory costs. Start by identifying the unit of work and the resource that limits it.

Four useful models

Model

Memory

Good first fit

Main cost or risk

Python threads

Shared

Waiting for files, networks, or other I/O

The GIL limits parallel Python bytecode

Native-library threads

Shared

NumPy/BLAS or compiled kernels that release the GIL

Oversubscription and race conditions

Python processes

Separate

Independent CPU-bound Python tasks

Serialization, copying, and process startup

Dask local scheduling

Threads or processes

A graph of tasks or chunked array/table work

Scheduler overhead and poor chunking

Concurrency means multiple tasks can make progress. Parallelism means work is executing at the same instant on multiple resources. Threads can provide useful concurrency even when they cannot execute Python bytecode in parallel.

The Global Interpreter Lock

In the standard CPython runtime, a thread must hold the Global Interpreter Lock (GIL) while executing Python bytecode. Two threads in one process therefore do not normally accelerate a CPU-bound loop written in Python.

The important boundary is not “Python versus threads,” but who executes the hot work:

  • a Python loop typically holds the GIL;

  • I/O releases the thread while it waits;

  • many NumPy, compression, and other native kernels release the GIL;

  • a separate process has its own interpreter and GIL.

This is why a threaded Dask scheduler can accelerate a native array kernel but fail to accelerate the same loop expressed as Python objects.

Runtime costs

The serial-work bound introduced in episode 1 is only the ideal limit. Real execution also adds task creation, scheduling, data transfer, allocation, and synchronization. A thousand microsecond tasks are often worse than a few tasks that each do substantial work. More workers can also make performance worse when each worker starts its own multithreaded BLAS kernel.

Control oversubscription

For predictable measurements, record worker counts and native thread settings such as OMP_NUM_THREADS, MKL_NUM_THREADS, or OPENBLAS_NUM_THREADS. A common starting point is one native thread per process.

Decision exercise

Exercise

Choose a first implementation for each workload and name one measurement that could change your decision.

  1. Download 500 independent files.

  2. Apply a NumPy universal function to four large array chunks.

  3. Run a CPU-heavy pure-Python parser on 100 independent files.

  4. Accelerate one deeply nested numerical loop called thousands of times.

Race-condition exercise

Exercise

Why is “threads share memory” both an advantage and a risk? Describe a safer shape for a parallel sum than having every worker update one shared total.

Keypoints

  • Choose an execution model from the bottleneck and data-sharing needs.

  • The GIL limits simultaneous Python bytecode, not all native work called from Python.

  • Processes bypass the single-process GIL but add startup and data-transfer costs.

  • Parallel speedup is bounded by serial work and overhead; measure with realistic task sizes.

See also

Use the selection guide as a quick reference, then continue with What problems fit Cython and Dask?.