Deep Engineering

MEASUREMENT

bench/iterators/memory_when_not.py

The script that produced the numbers in the article, and the record of the run. The file is read from the repository at build time — this is the code that was run, not a copy of it.

Cited in
/en/interview/python/iterators-and-generators
How to run it
for v in 3.11 3.12 3.13 3.14; do echo "== $v"; python$v protocol.py; done

The run below is recorded in Russian. It is a lab record, kept in the language it was written in; the numbers, the tables and the code read the same either way.

Record of the run

Замеры для урока «Итераторы и генераторы»

скрипт что показывает
protocol.py iter(x) is x для списка и итератора, исчерпание, состояния генератора
silent.py ошибка, которая не падает: два прохода по аргументу дают 0 вместо 19
memory_time.py пик памяти (tracemalloc) и время для списка против генератора на 1 000 000 и на 10 элементах
traps.py PEP 479 (RuntimeError и __cause__), yield from против ручного цикла, in и len на генераторе
versions.py запись __future__.generator_stop и возврат значения из close()
inlining.py PEP 709 структурно: <listcomp>, MAKE_FUNCTION и глубина стека — без секундомера

Запускать на всех версиях, которые есть:

for v in 3.11 3.12 3.13 3.14; do echo "== $v"; python$v protocol.py; done

Практика урока

practice.py — источник ответов двух практических задач урока, а runs/practice.txt — дословная запись его прогона. Ответ задачи не сочиняется: сборка сверяет заявленное с этой записью (scripts/validate-practice.mjs) и не проходит, если они разошлись.

Прогон снят 30.08.2026 на CPython 3.13.7 (Clang 20.1.4). Абсолютные числа — этой машины; переносится кратность, и задача «во сколько раз» стоит именно на ней.

Кратность устойчива только потому, что формы меряются ВПЕРЕМЕЖКУ: в каждом круге меряются все, минимум для каждой берётся по кругам. Пока замеры шли подряд, просадка машины в окне одной формы целиком доставалась ей, и отношение гуляло в полтора раза от запуска к запуску (измерено на декораторах: 5,9 / 7,1 / 7,5 / 9,2). После перехода на чередование расхождение между прогонами не выходит за несколько процентов. Перезаписывать запись прогона имеет смысл только вместе с проверкой задачи: если после перезапуска ответ изменился, менять нужно задачу, а не файл.

Правило, из-за которого половина замеров сделана структурно

3.11 и 3.12 собраны GCC 13.3.0, а 3.13 и 3.14 — Clang 20.1.4. Сравнивать время между этими парами нельзя: разница сборок перекрывает разницу версий; это общее правило замеров, а не оговорка одного этого каталога.

Поэтому эффект PEP 709 в уроке показан не секундомером, а inlining.py: наличием объекта кода <listcomp>, инструкции MAKE_FUNCTION и глубиной стека внутри включения. Эти признаки от тулчейна не зависят вовсе.

Сравнение «генератор против списка» при этом остаётся честным всегда: оно делается внутри одного запуска одного интерпретатора.

Script

102 lines
"""Когда ленивость памяти НЕ экономит.

ЗАЧЕМ ЭТОТ ФАЙЛ. Урок говорил: «генератор выигрывает память всегда». Замер, на
который он опирался, честный, но узкий: миллион ВЫЧИСЛЯЕМЫХ значений против
того же миллиона, материализованного списком. Слово «всегда» превращает это
наблюдение в свойство языка — а оно неверно ровно тогда, когда материализовать
нечего.

Здесь тот же инструмент (tracemalloc вокруг участка) применён к трём случаям,
и разница между ними — не в генераторе, а в том, есть ли альтернатива,
которая занимает память.

ПОЧЕМУ ПИКОВАЯ, А НЕ ТЕКУЩАЯ ПАМЯТЬ. Вопрос «сколько это стоило» — про
максимум, который программа держала одновременно: именно он определяет, влезет
ли работа в машину. tracemalloc.get_traced_memory() возвращает обе величины,
берётся вторая.

ЗАПУСК: python3.13 bench/iterators/memory_when_not.py
Вывод по версиям: runs/memory_when_not-3.11.txt и соседние.
"""

import sys
import tracemalloc

N = 1_000_000


def peak(fn) -> int:
    """Пик выделенной памяти на время работы fn, в байтах."""
    tracemalloc.start()
    tracemalloc.reset_peak()
    fn()
    _, high = tracemalloc.get_traced_memory()
    tracemalloc.stop()
    return high


def human(size: int) -> str:
    if size >= 1024 * 1024:
        return f"{size / 1024 / 1024:.2f} МиБ"
    if size >= 1024:
        return f"{size / 1024:.2f} КиБ"
    return f"{size} Б"


def show(title: str) -> None:
    print()
    print(title)
    print("-" * len(title))


def row(label: str, value: str) -> None:
    print(f"  {label:<52} {value}")


def main() -> None:
    print(f"Python {sys.version.split()[0]} ({sys.implementation.name})")
    print(f"N = {N:_}")

    show("1. Случай урока: альтернатива материализует весь набор")
    listcomp = peak(lambda: sum([i * i for i in range(N)]))
    genexp = peak(lambda: sum(i * i for i in range(N)))
    row("sum([i * i for i in range(N)])", human(listcomp))
    row("sum(i * i for i in range(N))", human(genexp))
    row("разница", f"{human(listcomp - genexp)} в пользу ленивого")

    show("2. Данные уже есть в памяти: экономить нечего")
    data = list(range(N))
    over_list = peak(lambda: sum(data))
    over_genexp = peak(lambda: sum(x for x in data))
    row("sum(data) по готовому списку", human(over_list))
    row("sum(x for x in data) по нему же", human(over_genexp))
    row("разница", f"{human(abs(over_list - over_genexp))}")
    row("почему", "список уже занят до замера; генератор его не отменяет")

    show("3. Результат всё равно нужен целиком: ленивость лишь откладывает")
    from_listcomp = peak(lambda: [i * i for i in range(N)])
    from_genexp = peak(lambda: list(i * i for i in range(N)))
    row("[i * i for i in range(N)]", human(from_listcomp))
    row("list(i * i for i in range(N))", human(from_genexp))
    row("вывод", "оба держат миллион значений; выигрыша нет")

    show("4. Генератор держит кадр и ссылки — это тоже память")
    def gen():
        buffer = [0] * 100_000          # то, что генератор удерживает между yield
        for i in range(10):
            yield i, len(buffer)

    def make_many():
        alive = [gen() for _ in range(100)]
        for g in alive:
            next(g)                      # запускаем: кадр и буфер созданы
        return alive

    hundred_generators = peak(make_many)
    row("сто приостановленных генераторов с буфером", human(hundred_generators))
    row("вывод", "приостановленный генератор — живой кадр, а не ноль")


if __name__ == "__main__":
    main()