58
Python dictionary прошлое, настоящее, будущее Dmitry Alimov Senior Software Engineer Zodiac Interactive 2016 SPb Python Interest Group

Python dict: прошлое, настоящее, будущее

Embed Size (px)

Citation preview

Page 1: Python dict: прошлое, настоящее, будущее

Python dictionary прошлое, настоящее, будущее

Dmitry Alimov Senior Software Engineer

Zodiac Interactive

2016

SPb Python Interest Group

Page 2: Python dict: прошлое, настоящее, будущее

Словарь в Python

Page 3: Python dict: прошлое, настоящее, будущее

>>> 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}

Page 4: Python dict: прошлое, настоящее, будущее

Ключами словаря могут быть значения только 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

Page 5: Python dict: прошлое, настоящее, будущее

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

Page 6: Python dict: прошлое, настоящее, будущее

Прошлое

Page 7: Python dict: прошлое, настоящее, будущее

Ячейка в хэш-таблице может иметь три состояния: 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

Page 8: Python dict: прошлое, настоящее, будущее

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]; };

Page 9: Python dict: прошлое, настоящее, будущее

Нужны хорошие хэш-функции

>>> 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]

Хэш-функции

Page 10: Python dict: прошлое, настоящее, будущее

Разрешение коллизий

Коллизия – ситуация, при которой разные входные значения (ключи) имеют одинаковое значение хэша. Процедура выбора подходящей ячейки для вставки элемента в хэш-таблицу называется пробирование, а рассматриваемая ячейка-кандидат – проба. В 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 использовался расчёт индекса основанный на многочленах

Page 11: Python dict: прошлое, настоящее, будущее

>>> 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

Page 12: Python dict: прошлое, настоящее, будущее

mask = PyDict_MINSIZE - 1 index = hash(123) & mask

Целые

Page 13: Python dict: прошлое, настоящее, будущее

Строки

mask = PyDict_MINSIZE - 1 index = hash(123) & mask

Page 14: Python dict: прошлое, настоящее, будущее

Словарь в 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

Page 15: Python dict: прошлое, настоящее, будущее

Добавление элемента

Page 16: Python dict: прошлое, настоящее, будущее

Добавление элемента

Page 17: Python dict: прошлое, настоящее, будущее

Добавление элемента

Page 18: Python dict: прошлое, настоящее, будущее

Добавление элемента

Page 19: Python dict: прошлое, настоящее, будущее

Добавление элемента

Page 20: Python dict: прошлое, настоящее, будущее

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

Page 21: Python dict: прошлое, настоящее, будущее

>>> d {'python': 2, 'article': 4, '!!!': 5, 'dict': 3, 'a key': 1} >>> d.__sizeof__() 248

Добавление элемента

Page 22: Python dict: прошлое, настоящее, будущее

Измерение размера хэш-таблицы

>>> d {'!!!': 5, 'python': 2, 'dict': 3, 'a key': 1, 'article': 4, ';)': 6} >>> d.__sizeof__() 1016

Page 23: Python dict: прошлое, настоящее, будущее

Измерение размера хэш-таблицы

/* 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

Page 24: Python dict: прошлое, настоящее, будущее

Порядок добавления ключей

>>> 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']

Индексы добавляемых в словарь элементов зависят от находящихся в нём элементов

Page 25: Python dict: прошлое, настоящее, будущее

>>> 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

Page 26: Python dict: прошлое, настоящее, будущее

>>> 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

Добавление элемента

во время итерации

Page 27: Python dict: прошлое, настоящее, будущее

Удаление элемента

dummy = PyString_FromString("<dummy key>"));

Page 28: Python dict: прошлое, настоящее, будущее

Интересный случай

Page 29: Python dict: прошлое, настоящее, будущее

Интересный случай

ma_fill = 6 > (8 * 2 / 3) dictresize()

Page 30: Python dict: прошлое, настоящее, будущее

Интересный случай

ma_fill = 6 > (8 * 2 / 3) ma_used = 1

отсюда minused = 4 * 1 = 4, следовательно newsize = 8

Page 31: Python dict: прошлое, настоящее, будущее

Кэш

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 нс

Page 32: Python dict: прошлое, настоящее, будущее

Открытая адресация vs метод цепочек

Хотя тут линейное пробирование, а не псевдослучайное как в CPython.

Page 33: Python dict: прошлое, настоящее, будущее

OrderedDict

from collections import OrderedDict

- Внутренний словарь - Кольцевой (циклический, замкнутый) двусвязный список - “/Lib/collections/__init__.py”

Page 34: Python dict: прошлое, настоящее, будущее

Настоящее

Page 35: Python dict: прошлое, настоящее, будущее

Словарь в 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;

Page 36: Python dict: прошлое, настоящее, будущее

Комбинированная таблица vs cплит-таблица

Комбинированная таблица - Для хранения всех явно созданных словарей (dict() и {}) - ma_values = NULL - Никогда не может стать сплит-таблицей Сплит-таблица - Для хранения __dict__ объектов - Ключи – только строки - Отдельная таблица для значений (ma_values) - После изменения размера превращается в комбинированную (но если

изменение размера происходит из-за setattr и существует только один экземпляр класса, происходит ре-сплит)

- Для поиска используется lookdict_split

Page 37: Python dict: прошлое, настоящее, будущее

Словарь в 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;

Page 38: Python dict: прошлое, настоящее, будущее

Сплит-таблица

Начальный размер = 4 Максимальное заполнение = (2*n+1)/3 = (2*4+1)/3 = 3, то есть изначально ma_keys->dk_usable = 3

Page 39: Python dict: прошлое, настоящее, будущее

Сплит-таблица

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)

Page 40: Python dict: прошлое, настоящее, будущее

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

Сплит-таблица

Сплит-таблица превратилась в комбинированную таблицу

Page 41: Python dict: прошлое, настоящее, будущее

Ключевые отличия от 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)

Резюме

Page 42: Python dict: прошлое, настоящее, будущее

Хэш-функции в 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

Page 43: Python dict: прошлое, настоящее, будущее

OrderedDict в CPython 3.5

- Двусвязный список - Хэш таблица od_fast_nodes c зеркальным отображением словаря od_dict - “/Include/odictobject.h”, “/Objects/odictobject.c”

Page 44: Python dict: прошлое, настоящее, будущее

Альтернативные версии

Page 45: Python dict: прошлое, настоящее, будущее

Словарь в 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; }

Page 46: Python dict: прошлое, настоящее, будущее

Словарь в PyPy

struct dicttable { variable_int *indexes; dictentry *entries; ... }

FREE = 0 DELETED = 1 VALID_OFFSET = 2

Page 47: Python dict: прошлое, настоящее, будущее

PyDictionary в Jython

- Построен на ConcurrentHashMap - Разрешения коллизий методом цепочек (separate chaining) - Начальный размер = 16, коэффициент заполнения = 0.75, коэффициент роста = 2 - Сегменты и потокобезопасность

Page 48: Python dict: прошлое, настоящее, будущее

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}

Page 49: Python dict: прошлое, настоящее, будущее

Будущее

Page 50: Python dict: прошлое, настоящее, будущее

Raymond Hettinger доволен

Page 51: Python dict: прошлое, настоящее, будущее

Словарь в 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, только для сплит-таблиц)

Page 52: Python dict: прошлое, настоящее, будущее

Словарь в 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 */ };

Page 53: Python dict: прошлое, настоящее, будущее

Словарь в CPython 3.6 (Комбинированная таблица)

Page 54: Python dict: прошлое, настоящее, будущее

Ключевые отличия от 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)

Резюме

Page 55: Python dict: прошлое, настоящее, будущее

Ссылки 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

Page 56: Python dict: прошлое, настоящее, будущее

Q & A

@delimitry

spbpython.guru

SPb Python Interest Group

Page 57: Python dict: прошлое, настоящее, будущее

Дополнительные слайды

Page 58: Python dict: прошлое, настоящее, будущее

Разрешение коллизий методом цепочек

Разрешение коллизий методом открытой адресации (псевдослучайное пробирование)