ЗАМЕР
bench/slots/memory.py
Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.
- Цитируется в статье
- /ru/python/memory/slots
- Как запустить
Записи прогонов — в `runs/`: `memory.txt` и `layout.txt` для основной сборки 3.13, остальные с суффиксом версии. У `access.py` запись одна: время между версиями не сравнивается (см. корневой `bench/README.md`), и второй файл провоцировал бы ровно такое сравнение. ## Почему память меряется партией, а не объектом `sys.getsizeof` — это `tp_basicsize + tp_itemsize*ob_size`: размер самого объекта, без того, на что он ссылается. Словарь экземпляра лежит по ссылке, и в это число не входит. Для `__slots__` результат получается не просто неточным, а бесполезным, причём по-разному в разных версиях: | `sys.getsizeof` | 3.11.15 | 3.12.3 | 3.13.13 | 3.14.7 | | --- | --- | --- | --- | --- | | без `__slots__` | 56 | 48 | 48 | 48 | | со `__slots__` | 56 | 56 | 56 | 56 | | что из этого следует | «разницы нет» | «слоты дороже» | «слоты дороже» | «слоты дороже» | | на самом деле (замер) | слоты дешевле на 40 | на 32 | на 40 | на 40 | На 3.12 и дальше число **перевёрнуто**: по нему слоты выходят дороже на 8 байт. На 3.11 оно просто молчит — одинаковое в обоих случаях. Ни в одной версии оно не даёт правильного ответа, и ни в одной не даёт его с одной и той же ошибкой. Честный ответ даёт `tracemalloc` вокруг создания партии в 200 000 экземпляров: он считает все запрошенные аллокации. Список-держатель отводится целиком **до** старта `tracemalloc`, поэтому в измерение не попадает и вычитать из суммы нечего — проверка 3 в блоке 5 `memory.py` меряет партию из уже созданного объекта и обязана дать ноль. Первый черновик скрипта вычитал «8 байт на ячейку списка»: так методика описана в плане статьи, и там она верна для варианта с `append`, где список растёт под измерением. Здесь он не растёт. Проверка и существует затем, чтобы такие расхождения не оставались на слово. ## Что измерено Класс с тремя атрибутами, байт на экземпляр: | | 3.11.15 | 3.12.3 | 3.13.13 | 3.14.7 | | --- | --- | --- | --- | --- | | без `__slots__` | 96 | 88 | 96 | 96 | | со `__slots__` | 56 | 56 | 56 | 56 | | без слотов, после `vars(obj)` | 160 | 152 | 160 | 160 | | подкласс класса со слотами, без своих | 96 | 88 | **72** | 96 | Экономия — 40 байт (42 %) на трёх из четырёх сборок и 32 байта (36 %) на 3.12. **«Экономия выросла в 3.13» — формулировка, которая держится только на сравнении с 3.12.** Ряд не монотонный: 3.11 уже давал 96 байт, 3.12 сделал обычный экземпляр на 8 байт дешевле, 3.13 их вернул. В плане статьи это было записано как рост в 3.13, потому что 3.11 в таблице не было. ## Подкласс без своих слотов: 72 байта на 3.13 и 96 на 3.14 Самая известная ловушка темы ведёт себя в разных версиях по-разному, и разница не мелкая. Причина читается по одному флагу, `layout.py` печатает его: | | 3.13.13 | 3.14.7 | | --- | --- | --- | | обычный класс, `INLINE_VALUES` | да | да | | подкласс без своих слотов, `INLINE_VALUES` | **нет** | **да** | | байт на экземпляр | 72 | 96 | Флага `Py_TPFLAGS_INLINE_VALUES` до 3.13 не существует вовсе — в `object.h` версий 3.11 и 3.12 бит 2 не определён, и спрашивать его там бессмысленно. Первый черновик `layout.py` спрашивал и печатал «False», то есть выдавал за измеренный факт ответ про несуществующий флаг. Почему подкласс его не получает на 3.13 — написано в заголовке этой машины, `/usr/include/python3.13/internal/pycore_object.h`:
Запись прогона
Замеры: __slots__ — сколько экономит и чем это правильно мерить
Тема, где почти каждый привычный индикатор отвечает неправильно, причём по-разному. Здесь три скрипта, и каждый ловит свой промах.
| скрипт | что показывает |
|---|---|
memory.py |
честная память на экземпляр через tracemalloc на партии; рядом — sys.getsizeof, который даёт ответ с ОБРАТНЫМ знаком; цена __weakref__, которую не видит basicsize; четыре версии подряд |
layout.py |
из чего эти байты складываются: флаги типа, basicsize, dictoffset, weaklistoffset, плюс дословные фрагменты заголовков CPython этой машины |
access.py |
ускоряет ли __slots__ доступ к атрибуту (нет) и во что обходится обращение к vars(obj) (в запись — почти вчетверо) |
Запуск:
for v in 3.11 3.12 3.13 3.14; do
echo "== $v"; python$v bench/slots/memory.py
done
for v in 3.11 3.12 3.13 3.14; do
echo "== $v"; python$v bench/slots/layout.py
done
python3.13 bench/slots/access.py
Записи прогонов — в runs/: memory.txt и layout.txt для основной сборки
3.13, остальные с суффиксом версии. У access.py запись одна: время между
версиями не сравнивается (см. корневой bench/README.md), и второй файл
провоцировал бы ровно такое сравнение.
Почему память меряется партией, а не объектом
sys.getsizeof — это tp_basicsize + tp_itemsize*ob_size: размер самого
объекта, без того, на что он ссылается. Словарь экземпляра лежит по ссылке, и
в это число не входит. Для __slots__ результат получается не просто
неточным, а бесполезным, причём по-разному в разных версиях:
sys.getsizeof |
3.11.15 | 3.12.3 | 3.13.13 | 3.14.7 |
|---|---|---|---|---|
без __slots__ |
56 | 48 | 48 | 48 |
со __slots__ |
56 | 56 | 56 | 56 |
| что из этого следует | «разницы нет» | «слоты дороже» | «слоты дороже» | «слоты дороже» |
| на самом деле (замер) | слоты дешевле на 40 | на 32 | на 40 | на 40 |
На 3.12 и дальше число перевёрнуто: по нему слоты выходят дороже на 8 байт. На 3.11 оно просто молчит — одинаковое в обоих случаях. Ни в одной версии оно не даёт правильного ответа, и ни в одной не даёт его с одной и той же ошибкой.
Честный ответ даёт tracemalloc вокруг создания партии в 200 000 экземпляров:
он считает все запрошенные аллокации. Список-держатель отводится целиком
до старта tracemalloc, поэтому в измерение не попадает и вычитать из
суммы нечего — проверка 3 в блоке 5 memory.py меряет партию из уже
созданного объекта и обязана дать ноль.
Первый черновик скрипта вычитал «8 байт на ячейку списка»: так методика
описана в плане статьи, и там она верна для варианта с append, где список
растёт под измерением. Здесь он не растёт. Проверка и существует затем, чтобы
такие расхождения не оставались на слово.
Что измерено
Класс с тремя атрибутами, байт на экземпляр:
| 3.11.15 | 3.12.3 | 3.13.13 | 3.14.7 | |
|---|---|---|---|---|
без __slots__ |
96 | 88 | 96 | 96 |
со __slots__ |
56 | 56 | 56 | 56 |
без слотов, после vars(obj) |
160 | 152 | 160 | 160 |
| подкласс класса со слотами, без своих | 96 | 88 | 72 | 96 |
Экономия — 40 байт (42 %) на трёх из четырёх сборок и 32 байта (36 %) на 3.12.
«Экономия выросла в 3.13» — формулировка, которая держится только на сравнении с 3.12. Ряд не монотонный: 3.11 уже давал 96 байт, 3.12 сделал обычный экземпляр на 8 байт дешевле, 3.13 их вернул. В плане статьи это было записано как рост в 3.13, потому что 3.11 в таблице не было.
Подкласс без своих слотов: 72 байта на 3.13 и 96 на 3.14
Самая известная ловушка темы ведёт себя в разных версиях по-разному, и разница
не мелкая. Причина читается по одному флагу, layout.py печатает его:
| 3.13.13 | 3.14.7 | |
|---|---|---|
обычный класс, INLINE_VALUES |
да | да |
подкласс без своих слотов, INLINE_VALUES |
нет | да |
| байт на экземпляр | 72 | 96 |
Флага Py_TPFLAGS_INLINE_VALUES до 3.13 не существует вовсе — в object.h
версий 3.11 и 3.12 бит 2 не определён, и спрашивать его там бессмысленно.
Первый черновик layout.py спрашивал и печатал «False», то есть выдавал за
измеренный факт ответ про несуществующий флаг.
Почему подкласс его не получает на 3.13 — написано в заголовке этой машины,
/usr/include/python3.13/internal/pycore_object.h:
static inline PyDictValues *
_PyObject_InlineValues(PyObject *obj)
{
assert(Py_TYPE(obj)->tp_flags & Py_TPFLAGS_INLINE_VALUES);
assert(Py_TYPE(obj)->tp_flags & Py_TPFLAGS_MANAGED_DICT);
assert(Py_TYPE(obj)->tp_basicsize == sizeof(PyObject));
return (PyDictValues *)((char *)obj + sizeof(PyObject));
}
Значения кладутся ровно по адресу obj + sizeof(PyObject), а у подкласса там
уже лежат слоты базы. Массив значений не заводится вовсе, и экземпляр выходит
дешевле обычного: указатель на словарь остаётся пустым, пока в словарь не
напишут. На 3.14 флаг у такого подкласса стоит, и цена возвращается к 96.
Заголовков 3.14 в этой машине нет. Поэтому про неё утверждается только то, что
прочитано флагом в рантайме и измерено; на её исходник здесь не ссылаются, и
layout.py говорит об этом в выводе.
Для сравнения — как то же место выглядело в 3.12,
/usr/include/python3.12/internal/pycore_object.h:
typedef union {
PyObject *dict;
/* Use a char* to generate a warning if directly assigning a PyDictValues */
char *values;
} PyDictOrValues;
Одно слово на «или словарь, или значения» — против отдельного указателя на словарь и отдельных inline values в 3.13.
__weakref__: тот же промах на другой величине
Слабая ссылка на экземпляр со слотами не создаётся, пока '__weakref__' не
назван в слотах явно. Сколько это стоит и что показывает basicsize:
| 3.11.15 | 3.12.3 | 3.13.13 | 3.14.7 | |
|---|---|---|---|---|
| со слотами, байт | 56 | 56 | 56 | 56 |
и с '__weakref__', байт |
64 | 72 | 72 | 72 |
| цена | 8 | 16 | 16 | 16 |
разница в basicsize |
8 | 0 | 0 | 0 |
weaklistoffset |
40 | −32 | −32 | −32 |
С 3.12 цена вдвое выше, а basicsize перестал её показывать: ссылка уехала в
предзаголовок, до начала объекта. Отрицательный weaklistoffset — то место,
где это видно.
Время: что меряется и почему повторяется
access.py проверяет второе ходовое утверждение темы — «слоты ускоряют
доступ». На 3.13.13, лучшее из 7 раундов:
| со слотами | без слотов | после vars(obj) |
|
|---|---|---|---|
чтение o.a |
8,78 | 8,51 | 8,53 |
запись o.a = 5 |
8,33 | 8,36 | 32,37 |
Разница между слотами и inline values — около трёх процентов, и при чтении она
направлена не в пользу слотов. Утверждение «__slots__ ускоряет доступ»
замером не подтверждается. Зато подтверждается другое: обращение к
vars(obj) удорожает запись почти вчетверо (+287 %), а чтение не трогает.
Кратность здесь устойчивее числа. В одну сессию то же место дало +136 %, в другую — +296, +298 и +302 % в трёх прогонах подряд. Вывод от этого не зависит, и в статье он подан кратностью, а не числом.
Числа сравниваются только между собой: они получены одним процессом на одной
сборке. Между версиями время не сравнивается вовсе (корневой bench/README.md).
Операция повторяется 50 раз внутри одного витка timeit, и это не
украшение. Проверка 2 печатает оба режима рядом. Без амортизации: pass
12,36 нс, o.a со слотами 19,44, без слотов 19,56 — то есть на сигнал
приходится 36 % числа, остальное цикл, и «разница» между двумя последними
строками лежит внутри его шума. После амортизации дно метода — 1,43 нс против
измеряемых 8, и числа стали означать то, что должны.
Чем это меряли
Python 3.11.15 и 3.12.3 (GCC 13.3.0), 3.13.13 (GCC 13.3.0), 3.14.7 (Clang 22.1.3); Intel Xeon 2,80 ГГц, 2 vCPU. Байты между версиями сравнивать можно — это раскладка объекта. Время нельзя: у сборок разные компиляторы и разные флаги.
Скрипт
369 строк"""Сколько байт на экземпляр экономит `__slots__` — и почему `getsizeof` врёт.
ЗАЧЕМ ЭТОТ СКРИПТ. Про `__slots__` пишут «экономит память», и почти всегда
меряют это через `sys.getsizeof`. Он даёт ответ **с обратным знаком**: у
экземпляра со слотами число БОЛЬШЕ, чем у экземпляра без слотов. Не потому,
что слоты дороже, а потому, что `getsizeof` — это `tp_basicsize +
tp_itemsize*ob_size`, то есть только сам объект и ничего из того, на что он
ссылается. Словарь экземпляра лежит по ссылке, и в это число не попадает.
ЧТО ЗДЕСЬ МЕРЯЕТСЯ ЧЕСТНО. `tracemalloc` вокруг создания партии экземпляров:
он считает все запрошенные аллокации, а не размер одного заголовка.
Список, который держит партию, отведён ЦЕЛИКОМ до старта `tracemalloc`
(`[None] * n`), поэтому в измерение он не попадает вовсе и вычитать из суммы
нечего. Первый черновик этого скрипта вычитал «8 байт на ячейку списка» — так
методика описана в плане статьи, и там она верна для варианта с `append`, где
список растёт под измерением. Здесь список не растёт. Проверка 3 в блоке 6
существует именно затем, чтобы это не осталось на слово: она меряет партию из
УЖЕ созданного объекта и обязана дать ноль.
ПОЧЕМУ ПАРТИЕЙ, А НЕ ПО ОДНОМУ. Одиночное измерение утонет в шуме аллокатора:
арены, пулы и округления дают разброс больше самой величины. На партии в
200 000 эти эффекты усредняются, а деление на N возвращает цену экземпляра.
ЧТО ЗДЕСЬ ПРОВЕРЯЕТСЯ. Блок 6 — самопроверка методики, без неё числам верить
нельзя:
* величина не зависит от размера партии (200 000 против 400 000);
* значения атрибутов не попадают в счёт: во всех классах присваиваются
ОДНИ И ТЕ ЖЕ заранее созданные объекты, поэтому на экземпляр не
приходится ни одной аллокации под значение;
* партия из УЖЕ созданного объекта даёт ноль — то есть список-держатель
в измерение действительно не входит.
Времени здесь нет вовсе: байты между версиями сравнивать можно, время — нет
(bench/README.md).
ЗАПУСК:
for v in 3.11 3.12 3.13 3.14; do echo "== $v"; python$v bench/slots/memory.py; done
"""
import itertools
import sys
import tracemalloc
BATCH = 200_000
# Значения, которые получат ВСЕ экземпляры. Созданы один раз до всякого
# измерения: иначе в счёт попала бы память под сами значения, а не под
# экземпляры. Маленькие целые ещё и кешируются интерпретатором.
V1, V2, V3 = 1, 2, 3
class Plain:
"""Обычный класс: атрибуты живут в словаре экземпляра."""
def __init__(self, a, b, c):
self.a = a
self.b = b
self.c = c
class Slotted:
"""Тот же класс со слотами: атрибуты живут в самом экземпляре."""
__slots__ = ("a", "b", "c")
def __init__(self, a, b, c):
self.a = a
self.b = b
self.c = c
class SlottedWeak:
"""Те же слоты плюс `__weakref__`: без него слабая ссылка не создаётся."""
__slots__ = ("a", "b", "c", "__weakref__")
def __init__(self, a, b, c):
self.a = a
self.b = b
self.c = c
class SlottedBase:
__slots__ = ("a", "b", "c")
def __init__(self, a, b, c):
self.a = a
self.b = b
self.c = c
class HeirWithoutSlots(SlottedBase):
"""Подкласс без своих слотов: `__dict__` возвращается, экономия исчезает."""
def per_instance(factory, n=BATCH, touch_dict=False):
"""Байт на экземпляр: вся память, запрошенная при создании партии из n штук.
Список-держатель отведён до старта tracemalloc и под измерение не попадает,
поэтому вычитать из суммы нечего (проверка 3 в блоке 6).
"""
holder = [None] * n # отведён ЦЕЛИКОМ до замера: не растёт и не аллоцирует
tracemalloc.start()
before = tracemalloc.get_traced_memory()[0]
for i in range(n):
obj = factory(V1, V2, V3)
if touch_dict:
obj.__dict__ # материализует настоящий словарь # noqa: B018
holder[i] = obj
after = tracemalloc.get_traced_memory()[0]
tracemalloc.stop()
total = after - before
assert len(holder) == n
return total / n
def rule(title):
print(f"\n=== {title} ===\n")
def block_honest():
rule("БЛОК 1. ЧЕСТНАЯ ПАМЯТЬ НА ЭКЗЕМПЛЯР")
plain = per_instance(Plain)
slotted = per_instance(Slotted)
touched = per_instance(Plain, touch_dict=True)
heir = per_instance(HeirWithoutSlots)
print(f" партия: {BATCH} экземпляров, класс с тремя атрибутами")
print()
print(f" без __slots__ {plain:6.1f} Б")
print(f" со __slots__ {slotted:6.1f} Б")
print(f" без слотов, после обращения к __dict__ {touched:6.1f} Б")
print(f" подкласс класса со слотами, без своих {heir:6.1f} Б")
print()
saved = plain - slotted
print(f" экономия {saved:6.1f} Б "
f"({100 * saved / plain:.0f} %)")
print()
print(" Третья строка — не экзотика, а обычный путь: любой код, который")
print(" трогает vars(obj) или obj.__dict__, переводит экземпляр в неё.")
print(" Четвёртая — ловушка наследования: слоты у базы не наследуются")
print(" как запрет, и подкласс без своих __slots__ снова получает словарь.")
return plain, slotted, touched, heir
def block_naive():
rule("БЛОК 2. ЧТО ПОКАЗЫВАЕТ getsizeof")
p = Plain(V1, V2, V3)
s = Slotted(V1, V2, V3)
gp, gs = sys.getsizeof(p), sys.getsizeof(s)
print(f" sys.getsizeof(без слотов) {gp:6d} Б")
print(f" sys.getsizeof(со слотами) {gs:6d} Б")
print()
if gs > gp:
print(f" Со слотами БОЛЬШЕ на {gs - gp} байт. По этому числу вывод получается")
print(" обратный правильному.")
else:
print(f" Разница {gp - gs} Б в пользу слотов — но это всё равно не ответ:")
print(" см. строку про словарь ниже.")
print()
print(" Почему так. getsizeof — это tp_basicsize + tp_itemsize*ob_size:")
print(" размер САМОГО объекта, без того, на что он ссылается. Словарь")
print(" экземпляра лежит по ссылке и в это число не входит:")
print()
p.__dict__ # материализуем, чтобы было что показать # noqa: B018
print(f" getsizeof(экземпляра без слотов) {sys.getsizeof(p):6d} Б")
print(f" getsizeof(его __dict__) {sys.getsizeof(p.__dict__):6d} Б")
print(f" сумма {sys.getsizeof(p) + sys.getsizeof(p.__dict__):6d} Б")
print()
print(" Сумма ближе к правде, но ответом не становится, и блок 3 показывает")
print(" почему.")
return gp, gs
def block_keysharing():
rule("БЛОК 3. ПОЧЕМУ СУММА ДВУХ getsizeof ТОЖЕ НЕ ОТВЕТ")
# ВАЖНО: этот блок обязан идти ДО того, как будут созданы тысячи Plain, —
# иначе первый экземпляр уже не будет первым. Отсюда собственный класс.
class Fresh:
def __init__(self, a, b, c):
self.a, self.b, self.c = a, b, c
first = Fresh(V1, V2, V3)
size_first = sys.getsizeof(first.__dict__)
keep = []
for _ in range(1000):
o = Fresh(V1, V2, V3)
o.__dict__ # noqa: B018
keep.append(o)
size_steady = sys.getsizeof(Fresh(V1, V2, V3).__dict__)
print(" Сначала — что вообще разделяется при key-sharing. Не словарь:")
print()
x, y = Fresh(V1, V2, V3), Fresh(V1, V2, V3)
dx, dy = x.__dict__, y.__dict__
print(f" dx is dy {dx is dy}")
print(f" dx == dy {dx == dy}")
x.a = 999
print(f" после x.a = 999 → dy['a'] {dy['a']}")
print()
print(" Словари разные, и запись в один в другом не видна. Разделяется")
print(" ТАБЛИЦА КЛЮЧЕЙ: имена атрибутов у всех экземпляров класса одни и те")
print(" же, и хранить их по разу на экземпляр незачем. Значения при этом")
print(" остаются свои у каждого.")
print()
print(" А теперь то, ради чего блок написан: размер словаря экземпляра")
print(" зависит от того, разделены ключи или ещё нет, — то есть от истории")
print(" программы, а не от класса:")
print()
print(f" getsizeof(__dict__) у ПЕРВОГО экземпляра класса {size_first:6d} Б")
print(f" он же после тысячи других экземпляров {size_steady:6d} Б")
print()
obj_size = sys.getsizeof(first)
print(" Один и тот же класс, одни и те же три атрибута. Наивная сумма")
print(" getsizeof(obj) + getsizeof(obj.__dict__) даёт, соответственно:")
print()
print(f" в начале программы {obj_size} + {size_first} = {obj_size + size_first} Б")
print(f" в установившемся режиме {obj_size} + {size_steady} = {obj_size + size_steady} Б")
print()
measured = per_instance(Plain, touch_dict=True)
print(f" А замер партией того же случая даёт {measured:.0f} Б")
print()
hi, lo = obj_size + size_first, obj_size + size_steady
print(f" То есть наивная сумма завышает на {hi - measured:.0f} Б в начале и занижает")
print(f" на {measured - lo:.0f} Б потом. Один и тот же способ, один и тот же класс,")
print(" ответ зависит от того, когда его спросили. Поэтому и меряется")
print(" партия: в ней ключи уже разделены, как и в настоящей программе,")
print(" где экземпляров много.")
return size_first, size_steady, obj_size
def block_weakref():
rule("БЛОК 4. ВТОРОЙ СЛУЧАЙ ТОГО ЖЕ: ЦЕНА `__weakref__`")
plain_slots = per_instance(Slotted)
with_weak = per_instance(SlottedWeak)
print(" Слабая ссылка на экземпляр со слотами не создаётся, пока")
print(" '__weakref__' не назван в слотах. Сколько это стоит:")
print()
print(f" со слотами {plain_slots:6.1f} Б")
print(f" со слотами и '__weakref__' {with_weak:6.1f} Б")
print(f" цена слабой ссылки {with_weak - plain_slots:6.1f} Б")
print()
print(f" А что показывает basicsize:")
print(f" Slotted.__basicsize__ {Slotted.__basicsize__:6d}")
print(f" SlottedWeak.__basicsize__ {SlottedWeak.__basicsize__:6d}")
print(f" разница "
f"{SlottedWeak.__basicsize__ - Slotted.__basicsize__:6d}")
print(f" weaklistoffset {SlottedWeak.__weakrefoffset__:6d}")
print()
if SlottedWeak.__basicsize__ == Slotted.__basicsize__:
print(" basicsize не изменился, а память изменилась. Отрицательный")
print(" weaklistoffset говорит, куда всё уехало: ссылка легла в")
print(" ПРЕДЗАГОЛОВОК, до начала объекта, и в basicsize она не входит.")
print(" Это тот же промах, что и с getsizeof в блоке 2, но на другой")
print(" величине: привычное число молчит, а байты тратятся.")
else:
print(" Здесь basicsize ещё честен: ссылка лежит внутри объекта, и")
print(" разница видна и в нём, и в замере.")
def block_layout():
rule("БЛОК 5. ГДЕ ЛЕЖАТ АТРИБУТЫ")
p = Plain(V1, V2, V3)
s = Slotted(V1, V2, V3)
print(" со слотами:")
print(f" hasattr(obj, '__dict__') {hasattr(s, '__dict__')}")
print(f" тип дескриптора Slotted.a {type(Slotted.a).__name__}")
try:
s.d = 4
except AttributeError as exc:
print(f" obj.d = 4 AttributeError: {exc}")
print()
print(" без слотов:")
print(f" hasattr(obj, '__dict__') {hasattr(p, '__dict__')}")
print(f" 'a' in vars(obj) {'a' in vars(p)}")
p.d = 4
print(f" obj.d = 4 прошло, vars = {sorted(vars(p))}")
print()
print(" Слот — это дескриптор данных НА КЛАССЕ, а значение лежит по")
print(" фиксированному смещению в самом экземпляре. Отсюда и экономия, и")
print(" запрет на новые атрибуты: места под них не отведено.")
def block_checks(plain, slotted):
rule("БЛОК 6. ПРОВЕРКА МЕТОДИКИ")
print(" 1. Величина не должна зависеть от размера партии")
print()
small = per_instance(Plain, n=BATCH)
large = per_instance(Plain, n=BATCH * 2)
print(f" без слотов, партия {BATCH}: {small:6.1f} Б")
print(f" без слотов, партия {BATCH * 2}: {large:6.1f} Б")
print(f" расхождение: {abs(small - large):6.2f} Б")
print()
print(" 2. Значения атрибутов не попадают в счёт")
print()
# Тот же класс, но каждому экземпляру — СВОИ значения: три новые строки.
#
# ПОЧЕМУ ЧЕРЕЗ СЧЁТЧИК, А НЕ "x" * 20. Первый черновик писал именно так — и
# проверка показывала разницу 0,0 байта, то есть не проверяла ничего:
# `"x" * 20` из литералов сворачивается на компиляции в константу, и все
# экземпляры получали ОДИН И ТОТ ЖЕ объект. Строка должна собираться в
# рантайме из значения, которого на компиляции не существует.
counter = itertools.count()
own = per_instance(
lambda a, b, c: Plain(
f"value-{next(counter)}",
f"value-{next(counter)}",
f"value-{next(counter)}",
)
)
print(f" общие значения (как в блоке 1): {plain:6.1f} Б")
print(f" три своих строки на экземпляр: {own:6.1f} Б")
print(f" разница — это память под значения: {own - plain:6.1f} Б")
print(" В блоке 1 значения общие, поэтому там измерены только экземпляры.")
print()
print(" 3. Список-держатель в измерение не попадает")
print()
sentinel = object()
empty = per_instance(lambda a, b, c: sentinel)
print(f" партия из уже созданного объекта: {empty:6.1f} Б на ячейку")
print(" Ноль — как и должно быть: список отведён целиком до старта")
print(" tracemalloc, запись в готовую ячейку ничего не аллоцирует.")
print(" Поэтому из чисел блока 1 ничего не вычитается.")
print()
print(" 4. Слоты и словарь — величины разного порядка")
print()
print(f" без слотов {plain:.1f} Б, со слотами {slotted:.1f} Б, "
f"отношение {plain / slotted:.2f}")
def main():
print("__SLOTS__: СКОЛЬКО ЭКОНОМИТ НА САМОМ ДЕЛЕ")
print()
print(f"Python {sys.version.split()[0]}, {sys.implementation.name}")
print(f"Сборка: {sys.version.split('[', 1)[1].rstrip(']') if '[' in sys.version else '?'}")
print()
print("Байты между версиями сравнивать можно: это раскладка объекта, а не")
print("время. Время в этом файле не меряется вовсе.")
plain, slotted, touched, heir = block_honest()
block_naive()
ks = block_keysharing()
block_weakref()
block_layout()
block_checks(plain, slotted)
print()
print("Все строки выше напечатаны этой программой.")
if __name__ == "__main__":
main()