Python Performance Quiz: Memory, Big-O and Profiling Practice

Test your Python performance knowledge with 12 questions and three timed programs on reference counting, operation costs, bytecode, profiling and NumPy.

  • Course: Python study plan
  • Module: Memory, performance and the interpreter
  • Kind: Checkpoint — cleared at 70%
  • Reading time: 25 min
  • Runtime: CPython 3.11

Checkpoint — Memory, performance and the interpreter is the checkpoint that closes the Memory, performance and the interpreter module: a graded quiz and whole-program exercises, passed at 70%.

Instructions

This checkpoint covers the whole module: the object model with reference counting, the cycle collector, weak references and the memory levers; the cost model of the built-in operations and the loop shapes that hide a quadratic; bytecode, code objects, the four kinds of name load and the 3.11 specialising interpreter; measuring with perf_counter, timeit, cProfile and tracemalloc under proper hygiene; numeric performance with array, bytes and NumPy vectorisation; and the checklist for writing fast Python from the algorithm down to the hot loop.

How it works. Twelve questions and three programs. You need 70% on the questions and every program accepted to clear the module. You can retake it as often as you like; your best score counts.

Before you start, make sure you can answer these from memory:

  • What does del x do to the object x referred to, and when is an object actually freed?
  • Why can reference counting not free two objects that refer to each other?
  • What is the cost of x in lst versus x in s, and of lst.pop(0) versus deque.popleft()?
  • Why is LOAD_FAST cheaper than LOAD_GLOBAL?
  • Which statistic does timeit report, and why the minimum?
  • What is the difference between tottime and cumtime in a profile?
  • What does vectorisation remove from a numeric loop?

The three programs are a top-k selection over a large generated stream with a heap, an order-preserving de-duplication of a large generated list that only passes with constant-time membership, and a word-frequency tally over generated tokens with a stable tie order — each sized so that the wrong data structure exceeds the time limit.

Common questions

Why can reference counting not free two objects that refer to each other?

Each object holds a reference to the other, so neither count ever reaches zero, even when nothing outside refers to them. CPython's generational cycle collector finds such groups and frees them.

Why is LOAD_FAST cheaper than LOAD_GLOBAL?

LOAD_FAST reads a local from the frame's slot array by index; LOAD_GLOBAL looks the name up in the module's globals dict and then in the built-ins. Binding a hot global to a local turns those lookups into slot reads.

What does vectorisation remove from a numeric loop?

The per-element interpreter work: a boxed object allocated for each result, type dispatch on every operation, reference-count updates and the bytecode loop itself. The loop still runs, but in C over raw numbers.

Exercises

Top-k with a heap

Read n k seed. Generate xs = [rng.randrange(10**9) for _ in range(n)] with rng = random.Random(seed) and find the k largest values with heapq.nlargest — O(n log k), not a full sort. Print top <the k values, largest first, space-separated> and threshold <the k-th largest>.

Input: n k seed. Output: two lines.

10 3 1

prints

top 909925047 861425548 820096753
threshold 820096753

Order-preserving de-duplication

Read n seed. Generate words = [f"w{rng.randrange(20000)}" for _ in range(n)]. Remove duplicates keeping the first occurrence of each word, in O(n) with a seen set — a list membership test per word is O(n·d) and will not finish the hidden case. Print distinct <count>, first <the first five distinct words> and last <the last five distinct words> (fewer when there are fewer).

Input: n seed. Output: three lines.

12 1

prints

distinct 12
first w4402 w18651 w2067 w8358 w3863
last w15474 w12439 w6879 w3075 w15986

Frequencies with stable ties

Read n seed. Generate tokens = [f"t{rng.randrange(50)}" for _ in range(n)]. Count them with collections.Counter and print the five most frequent (fewer if there are fewer distinct) as <token> <count>, ordered by count descending and then by token ascending — most_common alone does not define the tie order, so sort explicitly. Finish with distinct <number of distinct tokens>.

Input: n seed. Output: five lines, then the count.

30 1

prints

t48 3
t24 2
t28 2
t31 2
t6 2
distinct 24

In this module: Memory, performance and the interpreter

← Writing fast Python — the checklist, from algorithm to micro-optimisation · The interview template — the round, the file, fast I/O and what is actually judged →