Презентация со встречи сообщества SPb Python Interest Group рассказывающая об устройстве словарей в Python.
В презентации рассмотрена работа словаря в CPython 2.x, словаря в CPython 3.x, а также рассмотрены изменения в CPython 3.6.
Помимо CPython рассмотрены версии словаря в альтернативных реализациях Python, таких как PyPy, IronPython и Jython.
10. Разрешение коллизий
Коллизия – ситуация, при которой разные входные значения (ключи) имеют
одинаковое значение хэша.
Процедура выбора подходящей ячейки для вставки элемента в хэш-таблицу
называется пробирование, а рассматриваемая ячейка-кандидат – проба.
В CPython – используется пробирование с псевдослучайным шагом
PERTURB_SHIFT = 5
perturb = hash(key)
while True:
j = (5 * j) + 1 + perturb
perturb >>= PERTURB_SHIFT
index = j % 2**i
См. “/Objects/dictobject.c”
В CPython <2.2 использовался расчёт индекса основанный на многочленах
11. >>> PyDict_MINSIZE = 8
>>> key = 123
>>> hash(key) % PyDict_MINSIZE
>>> 3
Расчѐт индекса
>>> mask = PyDict_MINSIZE - 1
>>> hash(key) & mask
>>> 3
Вместо деления по модулю используется логическая операция «И» и маска
Так получаются младшие биты хэша:
2 ** i = PyDict_MINSIZE, отсюда i = 3, т.е. достаточно трёх младших бит
hash(123) = 123 = 0b1111011
mask = PyDict_MINSIZE - 1 = 8 - 1 = 7 = 0b111
index = hash(123) & mask = 0b1111011 & 0b111 = 0b011 = 3
26. >>> d = {'a': 1}
>>> for i in d:
... d['new item'] = 123
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
RuntimeError: dictionary changed size during iteration
Добавление элемента
во время итерации
31. Кэш
PyDictEntry ma_smalltable[8];
На x86 с линейкой кэша в 64 байта – в одну линейку входит:
64 / (4 * 3) = 5.33 элементов PyDictEntry
typedef struct {
Py_ssize_t me_hash;
PyObject *me_key;
PyObject *me_value;
} PyDictEntry;
Оптимизация локальности и коллизии
См. “/Objects/dictnotes.txt”
Источник Время доступа
Кэш L1 1 нс
Кэш L2 4 нс
RAM 100 нс
32. Открытая адресация vs метод цепочек
Хотя тут линейное пробирование, а не псевдослучайное как в CPython.
33. OrderedDict
from collections import OrderedDict
- Внутренний словарь
- Кольцевой (циклический, замкнутый) двусвязный список
- “/Lib/collections/__init__.py”
35. Словарь в CPython 3.5
- PEP 412 - Key-Sharing Dictionary
- Может быть в одной из двух форм: комбинированная таблица и сплит-таблица
- Начальный размер = 4 (сплит-таблица) или 8 (комбинированная таблица)
- Максимальное заполнение = (2*n+1)/3
- Коэффициент роста = used*2 + capacity/2
- “/Objects/dict-common.h”, “/Include/dictobject.h”, “/Objects/dictobject.c”,
“/Objects/dictnotes.txt”
typedef struct {
Py_hash_t me_hash;
PyObject *me_key;
PyObject *me_value; /* only meaningful for combined tables */
} PyDictKeyEntry;
struct _dictkeysobject {
Py_ssize_t dk_refcnt;
Py_ssize_t dk_size;
dict_lookup_func dk_lookup;
Py_ssize_t dk_usable;
PyDictKeyEntry dk_entries[1];
};
typedef struct {
PyObject_HEAD
Py_ssize_t ma_used;
PyDictKeysObject *ma_keys;
PyObject **ma_values;
} PyDictObject;
36. Комбинированная таблица vs cплит-таблица
Комбинированная таблица
- Для хранения всех явно созданных словарей (dict() и {})
- ma_values = NULL
- Никогда не может стать сплит-таблицей
Сплит-таблица
- Для хранения __dict__ объектов
- Ключи – только строки
- Отдельная таблица для значений (ma_values)
- После изменения размера превращается в комбинированную (но если
изменение размера происходит из-за setattr и существует только один
экземпляр класса, происходит ре-сплит)
- Для поиска используется lookdict_split
37. Словарь в CPython 3.5
Новое состояние ячейки в хэш-таблице для сплит-таблицы:
1) Неиспользованная
2) Активная
3) Пустая
4) Ожидающая (me_key != NULL, me_key != dummy и me_value == NULL)
typedef struct {
Py_hash_t me_hash;
PyObject *me_key;
PyObject *me_value; /* only meaningful for combined tables */
} PyDictKeyEntry;
38. Сплит-таблица
Начальный размер = 4
Максимальное заполнение = (2*n+1)/3 = (2*4+1)/3 = 3,
то есть изначально ma_keys->dk_usable = 3
40. class A():
def __init__(self):
self.a = 1
self.b = 2
self.c = 3
a = A()
print(a.__dict__.__sizeof__()) # 72
b = A()
setattr(a, 'd', 4) # респлита нет из-за b
print(a.__dict__.__sizeof__()) # 456
Сплит-таблица
Сплит-таблица превратилась в комбинированную таблицу
41. Ключевые отличия от CPython 2.x:
- Таблица может быть разделена на Ключи и Значения
- Добавлено новое состояние ячейки
- Больше нет ma_smalltable в структуре
- Обычные словари стали немного больше
- Выигрыш в памяти до 60% для программ, использующих много ООП (в
соответствии с https://github.com/python/cpython/blob/3.5/Objects/dictnotes.txt)
Всё ещё случаются баги типа: Unbounded memory growth resizing split-table
dicts (https://bugs.python.org/issue28147)
Резюме
42. Хэш-функции в CPython 3.5
SipHash для строк (>= CPython 3.4)
- Стойкий к hash-flooding DoS атакам
- Успешно используется во многих других языках
Немного изменённые хэш-функции для float, int
PEP 456 – Secure and interchangeable hash algorithm
hash(float("+inf")) == 314159,
hash(float("-inf")) == -314159, а было -271828
43. OrderedDict в CPython 3.5
- Двусвязный список
- Хэш таблица od_fast_nodes c зеркальным отображением словаря od_dict
- “/Include/odictobject.h”, “/Objects/odictobject.c”
45. Словарь в PyPy
- Начиная с PyPy 2.5.0 по умолчанию – ordereddict
- Начальный размер 16
- Коэффициент заполнения до 2/3
- Коэффициент роста 4 (до 30000 элементов) или 2
- При удалении множества элементов выполняется уплотнение
- “/rpython/rtyper/lltypesystem/rordereddict.py”
struct dicttable {
int num_live_items;
int num_ever_used_items;
int resize_counter;
variable_int *indexes; // byte, short, int, long
dictentry *entries;
...
}
struct dictentry {
PyObject *key;
PyObject *value;
long hash;
bool valid;
}
47. PyDictionary в Jython
- Построен на ConcurrentHashMap
- Разрешения коллизий методом цепочек (separate chaining)
- Начальный размер = 16, коэффициент заполнения = 0.75, коэффициент роста = 2
- Сегменты и потокобезопасность
48. PythonDictionary в IronPython
- Построен на Dictionary (.NET)
- Разрешения коллизий методом цепочек
- Начальный размер = 0, коэффициент заполнения = 1.0
- Рехэшинг в случае если число коллизий >= 100
- Коэффициент роста = 2 (новый размер равен ближайшему большему простому числу)
из ряда primes = {3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107,… , 4999559, 5999471, 7199369}
51. Словарь в CPython 3.6
typedef struct {
Py_hash_t me_hash;
PyObject *me_key;
PyObject *me_value; /* only meaningful for combined tables */
} PyDictKeyEntry;
typedef struct {
PyObject_HEAD
Py_ssize_t ma_used; /* number of items in the dictionary */
uint64_t ma_version_tag; /* unique, changes when dict modified */
PyDictKeysObject *ma_keys;
PyObject **ma_values;
} PyDictObject;
- Добавили версию ma_version_tag (PEP 509 – Add a private version to dict)
- Начальный размер = 8 (для сплит-таблицы тоже)
- Максимальное заполнение = (2*n)/3
- Добавил INADA Naoki в https://bugs.python.org/issue27350
Состояния ячеек в хэш-таблице:
1) Неиспользованная (index == DKIX_EMPTY == -1)
2) Активная (index >= 0 , me_key != NULL и me_value != NULL)
3) Пустая (index == DKIX_DUMMY == -2, только для комбинированных таблиц)
4) Ожидающая (index >= 0 , me_key != NULL и me_value == NULL, только для сплит-таблиц)
52. Словарь в CPython 3.6
- Добавили dk_nentries и dk_indices
struct _dictkeysobject {
Py_ssize_t dk_refcnt;
Py_ssize_t dk_size; /* Size of the hash table (dk_indices) */
dict_lookup_func dk_lookup; /* Function to lookup in dk_indices */
Py_ssize_t dk_usable; /* Number of usable entries in dk_entries */
Py_ssize_t dk_nentries; /* Number of used entries in dk_entries */
union {
int8_t as_1[8];
int16_t as_2[4];
int32_t as_4[2];
#if SIZEOF_VOID_P > 4
int64_t as_8[1];
#endif
} dk_indices;
PyDictKeyEntry dk_entries[dk_usable]; /* using DK_ENTRIES macro */
};
54. Ключевые отличия от CPython 3.5:
- Добавили dk_indices с типом, зависящим от размера
- Добавили версию ma_version_tag (PEP 509)
- Изменили начальный размер для сплит-таблицы на 8
- Изменили максимальное заполнение на (2*n)/3
- При удалении из сплит-таблицы она становится комбинированной
- Решена проблема сохранения порядка **kwargs (PEP 468)
- Решена проблема сохранения порядка атрибутов класса (PEP 520)
- Использование памяти на 20-25% меньше по сравнению с CPython 3.5
(https://docs.python.org/3.6/whatsnew/3.6.html#other-language-changes)
Резюме
55. Ссылки
1. Реализация словаря в Python 2.7 https://habrahabr.ru/post/247843/
2. Python hash calculation algorithms http://delimitry.blogspot.com/2014/07/python-hash-calculation-algorithms.html
3. PEP 412 - Key-Sharing Dictionary https://www.python.org/dev/peps/pep-0412/
4. PEP 456 - Secure and interchangeable hash algorithm https://www.python.org/dev/peps/pep-0456/
5. Mirror of the CPython repository https://github.com/python/cpython/
6. Faster, more memory efficient and more ordered dictionaries on PyPy https://morepypy.blogspot.ru/2015/01/faster-
more-memory-efficient-and-more.html
7. PyDictionary (Jython API documentation) http://www.jython.org/javadoc/org/python/core/PyDictionary.html
8. Jython repository https://bitbucket.org/jython/jython
9. Теория и практика Java: Построение лучшей HashMap http://www.ibm.com/developerworks/ru/library/j-jtp08223/
10. Back to basics: Dictionary part 2, .NET implementation https://blog.markvincze.com/back-to-basics-dictionary-part-2-
net-implementation/
11. http://referencesource.microsoft.com/mscorlib/system/collections/generic/dictionary.cs.html
12. https://github.com/IronLanguages/main/blob/ipy-2.7-maint/Languages/IronPython/IronPython/
13. https://bitbucket.org/pypy/pypy/
14. https://twitter.com/raymondh
15. PEP 509 - Add a private version to dict https://www.python.org/dev/peps/pep-0509/
16. Compact and ordered dict http://bugs.python.org/issue27350
17. What’s New In Python 3.6 https://docs.python.org/3.6/whatsnew/3.6.html
18. PEP 468 - Preserving the order of **kwargs in a function https://www.python.org/dev/peps/pep-0468/
19. PEP 520 - Preserving Class Attribute Definition Order https://www.python.org/dev/peps/pep-0520/
Картинки с сайтов:
http://www.rcreptiles.com/blog/index.php/2008/06/28/read_the_operating_manual_first
http://kiwigamer450.deviantart.com/art/Back-to-The-Past-Logo-567858767
http://beyondplm.com/wp-content/uploads/2014/04/time-paradox-past-future-present.jpg
http://itband.ru/wp-content/uploads/2014/10/Future.jpg
https://en.wikipedia.org/wiki/Hash_table