JA EN
LearnCompilers & Runtimes
·★ MEMBER·11 min read

Why Python Is Slow — Said Precisely

Saying Python is slow bundles three unrelated complaints: the cost of a single operation, the way data is laid out in memory, and the fact that CPU work does not spread across threads. Objects and the eval loop, the real reason NumPy is fast, what the GIL actually protects, and how far free-threaded builds since PEP 703 get you — from zero assumed knowledge.

ModalitytextTasksystems

Don't stop at "it's slow"

Picture assembling furniture. In C or Rust you drive in the parts as the diagram says. In Python you stop at every single step to ask an attendant: what is this part, is this the right tool? Each check takes a moment. Ten thousand steps means ten thousand checks.

Everyone has heard some version of that analogy. The trouble is that it explains only part of what people mean when they say Python is slow. Take a real report of a slow job apart and you almost always find three separate things tangled together:

  1. Each operation carries overhead — the interpreted execution model, and the fact that every value is an object
  2. The data is laid out badly — scattered across the heap, so the CPU never gets to stretch its legs
  3. It doesn't spread across cores — the GIL means CPU-bound work gains nothing from extra threads

Different causes take different cures. The first responds to vectorization or pushing the loop into C; the second to changing your data structures; the third to processes or a free-threaded build. Stop at "well, it's Python" and you end up reaching for the most expensive remedy of all — a rewrite in another language — first. This article is a map for telling the three apart.

What one a + b actually costs

CPython, the reference implementation, first compiles your source to bytecode, then runs an evaluation loop that interprets those instructions one at a time. The front half of that is exactly the pipeline in Compilers from scratch; the difference is that nothing emits machine code at the end. The instruction list gets read aloud instead.

You can see the instructions with the standard library's dis:

>>> import dis
>>> dis.dis(lambda a, b: a + b)
  RESUME        0
  LOAD_FAST     0 (a)
  LOAD_FAST     1 (b)
  BINARY_OP     0 (+)
  RETURN_VALUE

Four instructions. Looks cheap. The expense is hiding inside that one BINARY_OP, because in Python every value is a heap-allocated object carrying a reference count and a pointer to its type. Integers are no exception, and since Python's int has no fixed width, it holds an array of digits internally. Even the value 1 is not a number sitting in a register — it is a number in a box on a shelf.

So a + b goes roughly like this. Look up the type of a. Call that type's addition function through a function pointer. Pull the operands out of their boxes (unboxing). Add. Allocate a fresh box on the heap for the result (boxing). Decrement the reference counts of anything now unused, freeing it if a count hits zero. Then dispatch to the next bytecode.

The actual addition is one CPU instruction. Everything else is overhead.

Splitting the slowness into terms

Take a loop doing that addition NN times, in Python and in C.

Tpy=N(cop+cdispatch+cunbox+calloc+crc),Tc=NcopT_{\text{py}} = N\,(c_{\text{op}} + c_{\text{dispatch}} + c_{\text{unbox}} + c_{\text{alloc}} + c_{\text{rc}}), \qquad T_{\text{c}} = N\,c_{\text{op}}
(1)

Here copc_{\text{op}} is the real work, cdispatchc_{\text{dispatch}} the cost of deciding which instruction comes next and jumping there, cunboxc_{\text{unbox}} getting operands out of their boxes, callocc_{\text{alloc}} building the result box, and crcc_{\text{rc}} the reference-count bookkeeping. Put in words, it says this: C pays for the work NN times; Python pays for the work plus a tax, NN times.

Two commonly-missed consequences fall out. First, the ratio is 1+(tax)/cop1 + (\text{tax})/c_{\text{op}}, so the cheaper the real operation, the worse Python looks. Integer addition is close to the worst case. Regular-expression matching or big-integer multiplication closes the gap, because the expensive part happens inside a single call into C and the tax gets paid once. There is no single "Python is N times slower" number precisely because that ratio depends on what you are doing.

Second, both sides carry the same factor of NN. Python's slowness is a constant factor. It does nothing to how the cost grows with NN.

Constant factors versus orders

This is the caveat that pays rent in production. Suppose Python costs you 50x, and compare a Python O(nlogn)O(n \log n) algorithm against a C O(n2)O(n^2) one:

50nlog2n<n2    n>50log2n50\,n\log_2 n < n^2 \iff n > 50\log_2 n

which says that somewhere in the hundreds, the good algorithm in the slow language overtakes the bad algorithm in the fast one — and real workloads usually live well past that crossover.

FIG 1Switch the vertical axis to log scale and the difference becomes obvious: a constant factor only shifts a curve up or down, while a different order changes its slope. What Python carries is the shift

So the first move is never a rewrite; it is lowering the order. Replacing an in against a list with a set turns O(n)O(n) into O(1)O(1), and that edit comes long before any port to C.

The bet inside the eval loop

Why doesn't that constant factor shrink? At the centre of it sits dynamic typing.

A compiler can decide that a given addition is always 64-bit integers and collapse it to one instruction. CPython cannot. What a holds is only knowable at runtime, and it is legal for the type to change mid-run. So the type check starts over every time.

The specializing adaptive interpreter introduced in CPython 3.11 (PEP 659) attacks exactly this. While running, it observes that a particular `BINARY_OP` has been int-plus-int every time, and rewrites that instruction in place into an int-only one. From then on it proceeds with a minimal type guard, falling back to the

What's behind this

§

Members-only from here

371 walkthroughs, 26 textbook chapters, 48 student units and 6 close readings — all included for $4.99/mo, with three new explainers every day. Cancel any time; access runs to the end of the period.

Already a member? Sign in to keep reading

Comments

Sign in to comment