ЁЯПл The SchoolтА║ЁЯПОя╕П PerformanceтА║ЁЯОТ рдзрдбрд╛ 05 тАФ Memory: рд╕рдЧрд│реЗ рдЕрдбрдерд│реЗ рдПрдХрджрдо, рдХреА рдПрдХрд╛ рд╡реЗрд│реА рдПрдХ?
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯОТ рдзрдбрд╛ 05 тАФ Memory: рд╕рдЧрд│реЗ рдЕрдбрдерд│реЗ рдПрдХрджрдо, рдХреА рдПрдХрд╛ рд╡реЗрд│реА рдПрдХ?

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 05 ┬╖ рдорд╛рдЧреЗ: lesson-04-complexity ┬╖ рдкреБрдвреЗ: lesson-06-caching


ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ

рдзрдбреЗ 01тАУ04, рдЕрдзрд┐рдХ memory: program рдПрдХрд╛ рд╡реЗрд│реА рдХрд┐рддреА рдзрд░реВрди рдареЗрд╡рддреЛ, рддреЗ tracemalloc рдиреЗ рдХрд╕реЗ рдореЛрдЬрд╛рдпрдЪреЗ, рдЖрдгрд┐ Python рдордзрд▓рд╛ рд╕рд░реНрд╡рд╛рдд рд╕реЛрдкрд╛ рдореЛрдард╛ рдЙрдкрд╛рдп тАФ рд╕рдЧрд│реЗ items рдзрд░реВрди рдареЗрд╡рдгрд╛рд▒реНрдпрд╛ list рдРрд╡рдЬреА рдПрдХрд╛ рд╡реЗрд│реА рдПрдХ item рдмрдирд╡рдгрд╛рд░рд╛ generator. perf/demo.py рдордзрд▓реЗ memory() рдЖрдгрд┐ perf/sim.py рдордзрд▓реЗ total_list, total_gen, peak_kb рдЖрдгрд┐ held_at_once.

ЁЯзТ 5 рд╡рд░реНрд╖рд╛рдВрдЪреНрдпрд╛ рдореБрд▓рд╛рд▓рд╛ рд╕рдордЬрд╛рд╡рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ

рдЕрдбрдерд│реНрдпрд╛рдВрдЪреНрдпрд╛ рд╢рд░реНрдпрддреАрд▓рд╛ 100,000 рдЕрдбрдерд│реЗ рд▓рд╛рдЧрддрд╛рдд (рдЦреВрдкрдЪ рд▓рд╛рдВрдм рд╢рд░реНрдпрдд ЁЯШД). рддреЗ рд╕рд╛рд╣рд┐рддреНрдпрд╛рдЪреНрдпрд╛ рдЦреЛрд▓реАрдд рдареЗрд╡рд▓реЗрд▓реЗ рдЕрд╕рддрд╛рдд. ЁЯПЪя╕П

рд╢рд░реНрдпрдд рддреАрдЪ. рдЙрддреНрддрд░ рддреЗрдЪ. рдкрдг рдкрджреНрдзрдд 2 рд▓рд╛ рдЬрд╡рд│рдЬрд╡рд│ рдЬрд╛рдЧрд╛рдЪ рд▓рд╛рдЧрдд рдирд╛рд╣реА.

рдпрд╛рдд рдПрдХ рдЕрдбрдЪрдг рдЖрд╣реЗ. рдкрджреНрдзрдд 2 рдордзреНрдпреЗ, рдПрдХрджрд╛ рдЕрдбрдерд│рд╛ рдЦреЛрд▓реАрдд рдкрд░рдд рдЧреЗрд▓рд╛ рдХреА рддреЛ рдореИрджрд╛рдирд╛рд╡рд░реВрди рдЧреЗрд▓рд╛рдЪ. рддреБрдореНрд╣рд╛рд▓рд╛ рд╕рдЧрд│реЗ рдЕрдбрдерд│реЗ рдкреБрдиреНрд╣рд╛ рдкрд╛рд╣рд╛рдпрдЪреЗ рдЕрд╕рддреАрд▓, рдХрд┐рдВрд╡рд╛ рдореЛрдЬрд╛рдпрдЪреЗ рдЕрд╕рддреАрд▓, рдХрд┐рдВрд╡рд╛ рд╡реЗрдЧрд│реНрдпрд╛ рдХреНрд░рдорд╛рдиреЗ рддреНрдпрд╛рд╡рд░реВрди рдЙрдбреНрдпрд╛ рдорд╛рд░рд╛рдпрдЪреНрдпрд╛ рдЕрд╕рддреАрд▓, рддрд░ рддреБрдореНрд╣рд╛рд▓рд╛ рдкрджреНрдзрдд 1 рд▓рд╛рдЧрддреЗ.

ЁЯЧ║я╕П рдЖрдХреГрддреА

flowchart LR
    src["range(100,000)"] --> lst["ЁЯУж list: [i*i for i in ...]<br/>holds 100,000 results at once"]
    src --> gen["ЁЯФБ generator: (i*i for i in ...)<br/>holds 1 result at a time"]
    lst --> sum1["sum тЖТ 333,328,333,350,000"]
    gen --> sum2["sum тЖТ 333,328,333,350,000"]
    lst --> peak["tracemalloc peak:<br/>list > 100├Ч generator"]

ЁЯЧ║я╕П рдХрд╛рдврд▓реЗрд▓реА рдЖрдХреГрддреА + рдПрдХ lab: https://school-edh.pages.dev/performance/lesson-diagrams.html#l05

тЭУ рдХрд╛рдп

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг "test file рд╕рд╣ рдорд╛рдЭреНрдпрд╛ laptop рд╡рд░ рдЪрд╛рд▓рд▓реЗ" рдЖрдгрд┐ "рдЦрд▒реНрдпрд╛ file рд╕рд╣ production рдордзреНрдпреЗ рдорд╛рд░рд▓реЗ рдЧреЗрд▓реЗ" рд╣реЗ рд╡реЗрдЧрд╡реЗрдЧрд│реНрдпрд╛ data рдЖрдХрд╛рд░рд╛рдВрдЪреЗ рдПрдХрдЪ program рдЖрд╣реЗ. рд╕рдВрдкреВрд░реНрдг 10 GB log list рдордзреНрдпреЗ рд╡рд╛рдЪрдгрд╛рд▒реНрдпрд╛ report рд▓рд╛ 10 GB рдЖрдгрд┐ рддреНрдпрд╛рд╣реВрди рдЬрд╛рд╕реНрдд рд▓рд╛рдЧрддрд╛рдд; рддреЛрдЪ report рдУрд│-рдУрд│ рд╡рд╛рдЪрддрд╛рдирд╛ рдХрд╛рд╣реА kilobytes рд▓рд╛рдЧрддрд╛рдд. рддреБрдореНрд╣реА рдПрдХрд╛ рд╡реЗрд│реА рдХрд╛рдп рдзрд░рддрд╛ рд╣реЗ рдорд╛рд╣реАрдд рдЕрд╕рдгреЗ, рд╣рд╛рдЪ рдмрд▒реНрдпрд╛рдЪрджрд╛ рдЪрд╛рд▓рдгрд╛рд▒реНрдпрд╛ job рдЖрдгрд┐ crash рд╣реЛрдгрд╛рд▒реНрдпрд╛ job рдордзрд▓рд╛ рдлрд░рдХ рдЕрд╕рддреЛ.

ЁЯФз рдХрд╕реЗ (рдпрд╛ repo рдордзреНрдпреЗ)

perf/sim.py рдордзрд▓реЗ total_list(n) [i * i for i in range(n)] рдЪреА рдмреЗрд░реАрдЬ рдХрд░рддреЗ тАФ рдПрдХ list; total_gen(n) (i * i for i in range(n)) рдЪреА рдмреЗрд░реАрдЬ рдХрд░рддреЗ тАФ рдПрдХ generator. peak_kb(fn, n) tracemalloc рд╕реБрд░реВ рдХрд░рддреЗ, function рдЪрд╛рд▓рд╡рддреЗ, peak рд╡рд╛рдЪрддреЗ рдЖрдгрд┐ tracing рдерд╛рдВрдмрд╡рддреЗ. held_at_once рд╣рд╛ count model рдЖрд╣реЗ: list рд╕рд╛рдареА n, generator рд╕рд╛рдареА 1. Demo рддреБрд▓рдирд╛ рд╣реЛ/рдирд╛рд╣реА рдореНрд╣рдгреВрди рдЫрд╛рдкрддреЛ, рдХрд╛рд░рдг рдЕрдЪреВрдХ bytes рддреБрдордЪреНрдпрд╛ Python version рд╡рд░ рдЕрд╡рд▓рдВрдмреВрди рдЕрд╕рддрд╛рдд.

ЁЯзк рдХрд░реВрди рдкрд╛рд╣рд╛

python3 perf/demo.py memory
python3 - <<'EOF'
import sys; sys.path.insert(0, "perf"); from sim import total_list, total_gen, peak_kb
g = (i * i for i in range(5))
print("a generator runs once:", sum(g), "then", sum(g))
squares = [i * i for i in range(5)]
print("a list can be reused:", sum(squares), "then", sum(squares), "┬╖ length", len(squares), "┬╖ last", squares[-1])
for n in (10_000, 100_000, 1_000_000):
    lk, gk = peak_kb(total_list, n), peak_kb(total_gen, n)
    print(f"n {n:>9,}: list peak {lk / 1024:6.1f} MB ┬╖ generator peak {gk:5.1f} KB   (exact bytes vary by Python version)")
EOF

рд╢реЗрд╡рдЯрдЪреНрдпрд╛ рддреАрди рдУрд│реА рдЦрд░реА tracemalloc рдореЛрдЬрдорд╛рдкреЗ рдЖрд╣реЗрдд тАФ Mac рд╡рд░ Python 3.9 рд╡рд░ рддреА list рд╕рд╛рдареА рд╕реБрдорд╛рд░реЗ 0.3 MB, 3.7 MB рдЖрдгрд┐ 38.4 MB рдЖрдгрд┐ generator рд╕рд╛рдареА рд╕реБрдорд╛рд░реЗ 0.3 KB рд╣реЛрддреА. рддреБрдордЪреЗ рдЖрдХрдбреЗ рдереЛрдбреЗ рд╡реЗрдЧрд│реЗ рдЕрд╕рддреАрд▓; рдирдореБрдирд╛ (list n рд╕реЛрдмрдд рд╡рд╛рдврддреЗ, generator рд╡рд╛рдврдд рдирд╛рд╣реА) рдмрджрд▓рдгрд╛рд░ рдирд╛рд╣реА.

тЬЕ рддрдкрд╛рд╕рд╛ тАФ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд╛рдп рджрд┐рд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ

memory рд╣реЗ рдЫрд╛рдкрддреЗ:

тФАтФА the sum of squares of 0..99,999: a list holds every result, a generator makes one at a time
   same answer: True (333,328,333,350,000)
   results held at once: list 100,000 ┬╖ generator 1
   tracemalloc peak: the list needs over 100├Ч the generator's memory: True

рддреБрдордЪрд╛ snippet рдпрд╛рдиреЗ рд╕реБрд░реВ рд╣реЛрддреЛ:

a generator runs once: 30 then 0
a list can be reused: 30 then 30 ┬╖ length 5 ┬╖ last 16

ЁЯПБ рддреБрдореНрд╣реА рдЖрддреНрддрд╛рдЪ рдХрд╛рдп рд╕рд┐рджреНрдз рдХреЗрд▓реЗ

рдЙрддреНрддрд░ рддреЗрдЪ, рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ рд╡реЗрд│реА n рджрд╣рд╛ рдкрдЯ рд╡рд╛рдврд▓реНрдпрд╛рд╡рд░ list рдЪреА peak memory рджрд╣рд╛ рдкрдЯ рд╡рд╛рдврд▓реА, рддрд░ generator рдЪреА рд╕рдкрд╛рдЯ рд░рд╛рд╣рд┐рд▓реА тАФ рддреНрдпрд╛рдиреЗ рдХрдзреАрд╣реА рдПрдХрдЪ рд╡рд░реНрдЧ рдзрд░рд▓рд╛. рдкрдг generator рдЪрд╛ рджреБрд╕рд░рд╛ sum 0 рдЖрд▓рд╛: рддреЛ рд╕рдВрдкреВрди рдЧреЗрд▓рд╛ рд╣реЛрддрд╛. рдореНрд╣рдгреВрди рдирд┐рдпрдо "generators рдЪрд╛рдВрдЧрд▓реЗ рдЕрд╕рддрд╛рдд" рдЕрд╕рд╛ рдирд╛рд╣реА; рддреЛ рдЕрд╕рд╛ рдЖрд╣реЗ: "рдЬреЗ рдлрдХреНрдд рдПрдХрджрд╛рдЪ рдЪрд╛рд│рд╛рдпрдЪреЗ рдЖрд╣реЗ рддреЗ рдзрд░реВрди рдареЗрд╡реВ рдирдХрд╛".

тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд

рдЦрд▒реНрдпрд╛ machine рд╡рд░ тАФ рджреЛрди snapshots рд╡рд╛рдкрд░реВрди рдХреЛрдгрддреНрдпрд╛ рдУрд│реА рд╕рд░реНрд╡рд╛рдд рдЬрд╛рд╕реНрдд allocate рдХрд░рддрд╛рдд рддреЗ рд╢реЛрдзрд╛:

import tracemalloc
tracemalloc.start()
before = tracemalloc.take_snapshot()
build_results()
after = tracemalloc.take_snapshot()
for stat in after.compare_to(before, "lineno")[:5]:
    print(stat)          # file:line, size and count difference

рдореЛрдареНрдпрд╛ files рдЖрдгрд┐ query results load рдХрд░рдгреНрдпрд╛рдРрд╡рдЬреА stream рдХрд░рд╛:

with open("laps.csv") as f:              # one line at a time
    best = min(int(line.split(",")[2]) for line in f)

for row in cursor:                       # a DB cursor also streams (fetchmany under the hood)
    handle(row)

memray (Linux рдЖрдгрд┐ macOS) рдкреНрд░рддреНрдпреЗрдХ allocation рдиреЛрдВрджрд╡рддреЛ, native рд╕реБрджреНрдзрд╛, рдЖрдгрд┐ flame graph рдХрд╛рдврддреЛ:

python3 -m pip install memray
python3 -m memray run -o results.bin make_results.py
python3 -m memray flamegraph results.bin

рдПрдХрд╛рдЪ class рдЪреНрдпрд╛ рдЕрдиреЗрдХ рдЫреЛрдЯреНрдпрд╛ objects рд╕рд╛рдареА, __slots__ рдкреНрд░рддреНрдпреЗрдХ instance рдЪрд╛ __dict__ рдХрд╛рдвреВрди рдЯрд╛рдХрддреЛ рдЖрдгрд┐ memory рд╡рд╛рдЪрд╡рддреЛ:

class Lap:
    __slots__ = ("runner_id", "lap_ms")
    def __init__(self, runner_id, lap_ms): self.runner_id, self.lap_ms = runner_id, lap_ms

ЁЯПн Production рдордзреНрдпреЗ рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ рдЖрд╣реЗ: рдкреНрд░рддреНрдпреЗрдХ container рд╡рд░ memory limit рдареЗрд╡рд╛ рдЖрдгрд┐ input рдЪреНрдпрд╛ рдЖрдХрд╛рд░рд╛рд╢реЗрдЬрд╛рд░реА memory chart рдХрд░рд╛. Data рд╕реЛрдмрдд memory рд╡рд╛рдврдд рдЕрд╕реЗрд▓, рддрд░ stream рдЕрд╕рд╛рдпрд▓рд╛ рд╣рд╡реА рд╣реЛрддреА рддреА list рд╢реЛрдзрд╛.

тПня╕П рдкреБрдвреЗ

рдХрдзреА рдХрдзреА рд╕рд░реНрд╡рд╛рдд рдЬрд▓рдж рдХрд╛рдо рдореНрд╣рдгрдЬреЗ рдЬреЗ рддреБрдореНрд╣реА рдкреБрдиреНрд╣рд╛ рдХрд░рдд рдирд╛рд╣реА рддреЗ. рдкреБрдвреЗ: рд▓рдХреНрд╖рд╛рдд рдареЗрд╡рдгрд╛рд░рд╛ scoreboard тАФ caching рдЖрдгрд┐ memoization, рдЖрдгрд┐ hit ratio.

git checkout lesson-06-caching

ЁЯОТ Lesson 05 тАФ Memory: all the hurdles at once, or one at a time?

ЁЯУН You are here: Lesson 05 of 12 ┬╖ Previous: lesson-04-complexity ┬╖ Next: lesson-06-caching


ЁЯУж What's in this branch

Lessons 01тАУ04, plus memory: how much a program holds at once, how to measure it with tracemalloc, and the simplest big fix in Python тАФ a generator that makes one item at a time instead of a list that holds them all. memory() in perf/demo.py and total_list, total_gen, peak_kb and held_at_once in perf/sim.py.

ЁЯзТ Explain like I'm 5

The hurdles race needs 100,000 hurdles (a very long race ЁЯШД). They live in the equipment shed. ЁЯПЪя╕П

The race is the same. The answer is the same. But Way 2 needs almost no space.

There is one catch. With Way 2, once a hurdle has gone back to the shed, it is gone from the field. If you want to look at all the hurdles again, or count them, or jump them in a different order, you need Way 1.

ЁЯЧ║я╕П Diagram

flowchart LR
    src["range(100,000)"] --> lst["ЁЯУж list: [i*i for i in ...]<br/>holds 100,000 results at once"]
    src --> gen["ЁЯФБ generator: (i*i for i in ...)<br/>holds 1 result at a time"]
    lst --> sum1["sum тЖТ 333,328,333,350,000"]
    gen --> sum2["sum тЖТ 333,328,333,350,000"]
    lst --> peak["tracemalloc peak:<br/>list > 100├Ч generator"]

ЁЯЧ║я╕П Drawn version + a lab: https://school-edh.pages.dev/performance/lesson-diagrams.html#l05

тЭУ What

ЁЯдФ Why

Because "it worked on my laptop with the test file" and "it was killed in production with the real file" are the same program with different data sizes. A report that reads a whole 10 GB log into a list needs 10 GB and more; the same report reading line by line needs a few kilobytes. Knowing what you hold at once is often the difference between a job that runs and a job that crashes.

ЁЯФз How (in this repo)

total_list(n) in perf/sim.py sums [i * i for i in range(n)] тАФ a list; total_gen(n) sums (i * i for i in range(n)) тАФ a generator. peak_kb(fn, n) starts tracemalloc, runs the function, reads the peak and stops tracing. held_at_once is the count model: n for a list, 1 for a generator. The demo prints the comparison as a yes/no, because the exact bytes depend on your Python version.

ЁЯзк Try it

python3 perf/demo.py memory
python3 - <<'EOF'
import sys; sys.path.insert(0, "perf"); from sim import total_list, total_gen, peak_kb
g = (i * i for i in range(5))
print("a generator runs once:", sum(g), "then", sum(g))
squares = [i * i for i in range(5)]
print("a list can be reused:", sum(squares), "then", sum(squares), "┬╖ length", len(squares), "┬╖ last", squares[-1])
for n in (10_000, 100_000, 1_000_000):
    lk, gk = peak_kb(total_list, n), peak_kb(total_gen, n)
    print(f"n {n:>9,}: list peak {lk / 1024:6.1f} MB ┬╖ generator peak {gk:5.1f} KB   (exact bytes vary by Python version)")
EOF

The last three lines are real tracemalloc measurements тАФ on Python 3.9 on a Mac they were about 0.3 MB, 3.7 MB and 38.4 MB for the list and about 0.3 KB for the generator. Your numbers will differ a little; the pattern (the list grows with n, the generator does not) will not.

тЬЕ Verify тАФ what you should see

memory prints:

тФАтФА the sum of squares of 0..99,999: a list holds every result, a generator makes one at a time
   same answer: True (333,328,333,350,000)
   results held at once: list 100,000 ┬╖ generator 1
   tracemalloc peak: the list needs over 100├Ч the generator's memory: True

Your snippet starts with:

a generator runs once: 30 then 0
a list can be reused: 30 then 30 ┬╖ length 5 ┬╖ last 16

ЁЯПБ What you just proved

Same answer, and the list's peak memory grew ten times each time n grew ten times, while the generator's stayed flat тАФ it only ever held one square. But the generator's second sum was 0: it was used up. So the rule is not "generators are better"; it is "don't hold what you only need to walk through once".

тЪая╕П Common mistakes

ЁЯПн In production

On a real machine тАФ find which lines allocate the most, with two snapshots:

import tracemalloc
tracemalloc.start()
before = tracemalloc.take_snapshot()
build_results()
after = tracemalloc.take_snapshot()
for stat in after.compare_to(before, "lineno")[:5]:
    print(stat)          # file:line, size and count difference

Stream big files and query results instead of loading them:

with open("laps.csv") as f:              # one line at a time
    best = min(int(line.split(",")[2]) for line in f)

for row in cursor:                       # a DB cursor also streams (fetchmany under the hood)
    handle(row)

memray (Linux and macOS) records every allocation, including native ones, and draws a flame graph:

python3 -m pip install memray
python3 -m memray run -o results.bin make_results.py
python3 -m memray flamegraph results.bin

For many small objects of one class, __slots__ removes each instance's __dict__ and saves memory:

class Lap:
    __slots__ = ("runner_id", "lap_ms")
    def __init__(self, runner_id, lap_ms): self.runner_id, self.lap_ms = runner_id, lap_ms

ЁЯПн Why this matters in production: set a memory limit on every container and chart memory next to the size of the input. When memory grows with the data, find the list that should have been a stream.

тПня╕П Next

Sometimes the fastest work is work you don't do again. Next: the scoreboard that remembers тАФ caching and memoization, and the hit ratio.

git checkout lesson-06-caching
тЖР PreviouscomplexityNext тЖТcaching

This page is the lesson's README from the lesson-05-memory branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.