Python dictionary прошлое, настоящее, будущее
Dmitry Alimov Senior Software Engineer
Zodiac Interactive
2016
SPb Python Interest Group
Словарь в Python
>>> d = {} # то же самое, что d = dict()
>>> d['a'] = 123
>>> d['b'] = 345
>>> d['c'] = 678
>>> d
{'a': 123, 'c': 678, 'b': 345}
>>> d['b']
345
>>> del d['c']
>>> d
{'a': 123, 'b': 345}
Ключами словаря могут быть значения только hashable типов
>>> d[list()] = 1 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unhashable type: 'list' >>> d[set()] = 2 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unhashable type: 'set' >>> d[dict()] = 3 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unhashable type: 'dict'
Все иммутабельные built-in’ы – hashable
import random
class A(object):
def __init__(self, index):
self.index = index
def __eq__(self, other):
return True
def __hash__(self):
return random.randint(0, 3)
def __repr__(self):
return 'A%d' % self.index
d = {A(0): 0, A(1): 1, A(2): 2}
print('keys: %s' % d.keys())
print('values: %s' % d.values())
for k in d:
print('%s = %s' % (k, d.get(k, 'not found')))
Random hash – плохая идея
Запуск 1
keys: [A1, A2, A0]
values: [1, 2, 0]
A1 = 1
A2 = not found
A0 = 0
Запуск 2
keys: [A1, A0]
values: [2, 0]
A1 = not found
A0 = not found
Прошлое
Ячейка в хэш-таблице может иметь три состояния: 1) Неиспользованная 2) Активная 3) Пустая
typedef struct {
Py_ssize_t me_hash;
PyObject *me_key;
PyObject *me_value;
} PyDictEntry;
- Хэш-таблица - Разрешения коллизий методом открытой адресации - Начальный размер = 8 - Коэффициент заполнения = 2/3 - Коэффициент роста = 4 или 2 (зависит от числа используемых ячеек) - “/Include/dictobject.h”, “/Objects/dictobject.c”, “/Objects/dictnotes.txt”
Словарь в CPython >2.1
ma_fill – сумма «активных» и «пустых» ячеек ma_used – число «активных» ячеек ma_mask – маска, равная PyDict_MINSIZE - 1 ma_lookup – функция поиска (по умолчанию lookdict_string)
#define PyDict_MINSIZE 8 typedef struct _dictobject PyDictObject; struct _dictobject { PyObject_HEAD Py_ssize_t ma_fill; Py_ssize_t ma_used; Py_ssize_t ma_mask; PyDictEntry *ma_table; PyDictEntry *(*ma_lookup)(PyDictObject *mp, PyObject *key, long hash); PyDictEntry ma_smalltable[PyDict_MINSIZE]; };
Нужны хорошие хэш-функции
>>> map(hash, [0, 1, 2, 3, 4]) [0, 1, 2, 3, 4] >>> map(hash, ['abca', 'abcb', 'abcc', 'abcd', 'abce']) [1540938117, 1540938118, 1540938119, 1540938112, 1540938113]
Модифицированная хэш-функция FNV (Fowler–Noll–Vo) для строк
Ключ “-R” интерпретатора для псевдо-случайной соли (строки, bytes и объекты datetime)
>>> map(hash, ['abca', 'abcb', 'abcc', 'abcd', 'abce']) [-218138032, -218138029, -218138030, -218138027, -218138028]
Хэш-функции
Разрешение коллизий
Коллизия – ситуация, при которой разные входные значения (ключи) имеют одинаковое значение хэша. Процедура выбора подходящей ячейки для вставки элемента в хэш-таблицу называется пробирование, а рассматриваемая ячейка-кандидат – проба. В 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 использовался расчёт индекса основанный на многочленах
>>> 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
mask = PyDict_MINSIZE - 1 index = hash(123) & mask
Целые
Строки
mask = PyDict_MINSIZE - 1 index = hash(123) & mask
Словарь в CPython >2.1
Инициализация словаря
Добавление элемента
PyDict_SetItem()
PyDict_New() ma_used = 0 ma_fill = 0 ma_mask = PyDict_MINSIZE – 1 ma_table = ma_smalltable ma_lookup = lookdict_string
insertdict() ma_used += 1 ma_fill += 1 dictresize() если ma_fill >= 2/3 * size
Удаление элемента
PyDict_DelItem() ma_used -= 1
Добавление элемента
Добавление элемента
Добавление элемента
Добавление элемента
Добавление элемента
perturb = -1297030748 # i = (i * 5) + 1 + perturb i = (4 * 5) + 1 + (-1297030748) = -1297030727 index = -1297030727 & 7 = 1
hash('!!!') = -1297030748 i = -1297030748 & 7 = 4
# perturb = perturb >> PERTURB_SHIFT perturb = -1297030748 >> 5 = -40532211 # i = (i * 5) + 1 + perturb i = (-1297030727 * 5) + 1 + (-40532211) = -6525685845 index = -6525685845 & 7 = 3
>>> d {'python': 2, 'article': 4, '!!!': 5, 'dict': 3, 'a key': 1} >>> d.__sizeof__() 248
Добавление элемента
Измерение размера хэш-таблицы
>>> d {'!!!': 5, 'python': 2, 'dict': 3, 'a key': 1, 'article': 4, ';)': 6} >>> d.__sizeof__() 1016
Измерение размера хэш-таблицы
/* Find the smallest table size > minused. */ for (newsize = 8; newsize <= minused && newsize > 0; newsize <<= 1) ; ...
}
dictresize(PyDictObject *mp, Py_ssize_t minused) { ...
PyDict_SetItem(...) { ... dictresize(mp, (mp->ma_used > 50000 ? 2 : 4) * mp->ma_used); ... }
В нашем примере: ma_fill = 6 > (8 * 2 / 3) ma_used = 6
отсюда minused = 4 * 6 = 24, следовательно newsize = 32
Порядок добавления ключей
>>> d1 = {'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5} >>> d2 = {'three': 3, 'two': 2, 'five': 5, 'four': 4, 'one': 1} >>> d1 == d2 True >>> d1.keys() ['four', 'three', 'five', 'two', 'one'] >>> d2.keys() ['four', 'one', 'five', 'three', 'two']
Индексы добавляемых в словарь элементов зависят от находящихся в нём элементов
>>> 7.0 == 7 == (7+0j) True >>> d = {} >>> d[7.0] = 'float' >>> d {7.0: 'float'} >>> d[7] = 'int' >>> d {7.0: 'int'} >>> d[7+0j] = 'complex' >>> d {7.0: 'complex'} >>> type(d.keys()[0]) <type 'float'>
int, float, complex
>>> hash(7) 7 >>> hash(7.0) 7 >>> hash(7+0j) 7
>>> 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
Добавление элемента
во время итерации
Удаление элемента
dummy = PyString_FromString("<dummy key>"));
Интересный случай
Интересный случай
ma_fill = 6 > (8 * 2 / 3) dictresize()
Интересный случай
ma_fill = 6 > (8 * 2 / 3) ma_used = 1
отсюда minused = 4 * 1 = 4, следовательно newsize = 8
Кэш
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 нс
Открытая адресация vs метод цепочек
Хотя тут линейное пробирование, а не псевдослучайное как в CPython.
OrderedDict
from collections import OrderedDict
- Внутренний словарь - Кольцевой (циклический, замкнутый) двусвязный список - “/Lib/collections/__init__.py”
Настоящее
Словарь в 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;
Комбинированная таблица vs cплит-таблица
Комбинированная таблица - Для хранения всех явно созданных словарей (dict() и {}) - ma_values = NULL - Никогда не может стать сплит-таблицей Сплит-таблица - Для хранения __dict__ объектов - Ключи – только строки - Отдельная таблица для значений (ma_values) - После изменения размера превращается в комбинированную (но если
изменение размера происходит из-за setattr и существует только один экземпляр класса, происходит ре-сплит)
- Для поиска используется lookdict_split
Словарь в 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;
Сплит-таблица
Начальный размер = 4 Максимальное заполнение = (2*n+1)/3 = (2*4+1)/3 = 3, то есть изначально ma_keys->dk_usable = 3
Сплит-таблица
class A(): def __init__(self): self.a = 1 self.b = 2 self.c = 3 a = A() print(a.__dict__.__sizeof__()) # 72 setattr(a, 'd', 4) # респлит print(a.__dict__.__sizeof__()) # 168
print({}.__sizeof__()) # 264
Начальный размер = 4 Максимальное заполнение = (2*n+1)/3 = (2*4+1)/3 = 3 Коэффициент роста = used*2 + capacity/2 = 3*2 + 4/2 = 8, отсюда minused = 8, следовательно newsize = 16 (см. dictresize)
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
Сплит-таблица
Сплит-таблица превратилась в комбинированную таблицу
Ключевые отличия от 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)
Резюме
Хэш-функции в 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
OrderedDict в CPython 3.5
- Двусвязный список - Хэш таблица od_fast_nodes c зеркальным отображением словаря od_dict - “/Include/odictobject.h”, “/Objects/odictobject.c”
Альтернативные версии
Словарь в 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; }
Словарь в PyPy
struct dicttable { variable_int *indexes; dictentry *entries; ... }
FREE = 0 DELETED = 1 VALID_OFFSET = 2
PyDictionary в Jython
- Построен на ConcurrentHashMap - Разрешения коллизий методом цепочек (separate chaining) - Начальный размер = 16, коэффициент заполнения = 0.75, коэффициент роста = 2 - Сегменты и потокобезопасность
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}
Будущее
Raymond Hettinger доволен
Словарь в 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, только для сплит-таблиц)
Словарь в 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 */ };
Словарь в CPython 3.6 (Комбинированная таблица)
Ключевые отличия от 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)
Резюме
Ссылки 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
Q & A
@delimitry
spbpython.guru
SPb Python Interest Group
Дополнительные слайды
Разрешение коллизий методом цепочек
Разрешение коллизий методом открытой адресации (псевдослучайное пробирование)