Python dictionary прошлое, настоящее, будущее Dmitry Alimov Senior Software Engineer Zodiac Interactive 2016 SPb Python Interest Group
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
Дополнительные слайды
Разрешение коллизий методом цепочек
Разрешение коллизий методом открытой адресации (псевдослучайное пробирование)