Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Страшные встроенные структуры

Страшные встроенные структуры

Доклад, в котором я разбираю структуры list и dict: как они устроеные внутри и как разложены в памяти.

Denis Anikin

May 23, 2022
Tweet

More Decks by Denis Anikin

Other Decks in Programming

Transcript

  1. Денис Аникин
    https://xfenix.ru
    Страшные
    встроенные
    структуры

    View Slide

  2. Struct, array
    2

    View Slide

  3. Немного о памяти
    3
    — Память это массив байтов с точки зрения софта (byte-addressed). Для
    нас это бесконечный поток адресов конкретных байтов
    — Оперативная память хорошо параллелизирует операции

    View Slide

  4. Адреса в оперативной памяти
    4
    — Вы можете по ним перемещаться с помощью вычитания и сложения
    — По сути мы с вами оперируем несколькими большими сущностями: записями и массивами

    View Slide

  5. Запись
    5
    — Пример — pyobject

    View Slide

  6. Как получить поле записи?
    6
    Адрес записи + индекс нужного поля внутри * 8

    View Slide

  7. По сути мы описали struct в C
    7
    Вот его аналог в Python:

    View Slide

  8. Еще одна структура — array
    8
    Так мы можем сохранить строку:

    View Slide

  9. Модуль array
    9

    View Slide

  10. Обе структуры дают
    константный доступ к
    данным
    10

    View Slide

  11. Некоторые дополнительные специализированные
    массивы
    11
    — str
    — StringIO
    — memoryview
    — bytearray
    — bytes

    View Slide

  12. Кое-что еще
    12
    Каждый раз, когда вы хотите обратиться к элементам массивов, вы
    вынуждены собирать питон объекты. Если вы захотите сделать sum
    массива из 100 float, вы получите сборку >100 объектов

    View Slide

  13. List, tuple
    13

    View Slide

  14. Tuple
    14

    View Slide

  15. Важные дополнения
    15
    — При копировании мы всего-лишь переносим ссылки
    — Чтобы достать item, получить len, нам достаточно вытащить 8 байт!

    View Slide

  16. List!
    16
    С ним есть проблема

    View Slide

  17. Как list устроен внутри
    17

    View Slide

  18. Как list устроен внутри
    18

    View Slide

  19. Реалокация 😬
    19
    >>> list(range(10))
    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    >>> sum(range(10))
    45
    >>> sum(range(1000))
    499500
    >>> sum(range(1_000_000))
    499999500000

    View Slide

  20. Как же лист борется с append’ами?
    20
    — При аппенде в наполненный список добавляется не 1 слот, с каждым
    разом все больше: 4, 8, 16, 25, 35, 46, 58, 72, 88, …
    — Это называется амортизацией

    View Slide

  21. Интересные цифры
    21

    View Slide

  22. Есть и цена
    22
    Если у вы добавляете 991 первый элемент к списку из 990 элементов, то
    получаете аллокацию 1120 «слотов» и копирование 990 элементов «за
    занавеской»

    View Slide

  23. Не все так плохо:
    list использует 94%* места и
    требует около 6% экстра-места
    для будущих append
    *в среднем
    **кажется…
    23

    View Slide

  24. List — опасен!
    24
    s = [1, 2, 3, …]
    — s.insert(0, v)
    — s.pop(0)

    View Slide

  25. Плохая идея
    25
    while my_list:
    item = my_list.pop(0)
    print(item)

    View Slide

  26. Идея получше
    26
    for item in reversed(my_list):
    print(item)

    View Slide

  27. List — опасен!
    27
    s = [1, 2, 3, …] — 1000 элементов
    — Делаем s[400:500]
    — Получаем 800 байт копирования и новый список в 100 элементов!

    View Slide

  28. Dictionary
    28

    View Slide

  29. View Slide

  30. А почему dict так важен?
    30
    — globals
    — locals’
    — modules
    — class
    — instance

    View Slide

  31. Как нам превратить ключ в индекс?
    31
    У нас есть что угодно:
    — строка
    — число
    — tuple
    — и т.п.

    View Slide

  32. Как нам превратить ключ в индекс?
    32
    У нас есть что угодно:
    — строка
    — число
    — tuple
    — и т.п.
    Из этого должен получиться индекс!

    View Slide

  33. Hash!
    33

    View Slide

  34. View Slide

  35. Давайте поговорим о хеше
    35
    Что это вообще
    — Функция, возвращающая предсказуемое число на любые входные данные

    View Slide

  36. Давайте поговорим о хеше
    36
    Что это вообще
    — Функция, возвращающая предсказуемое число на любые входные данные
    — На 32битной платформе возвращет 32 бита, на 64 — 64

    View Slide

  37. Давайте поговорим о хеше
    37
    Что это вообще
    — Функция, возвращающая предсказуемое число на любые входные данные
    — На 32битной платформе возвращет 32 бита, на 64 — 64
    — У каждого типа свое хеширование.

    View Slide

  38. Что хешируемо?
    38
    — int
    — str
    — tuple
    — frozenset
    — functions (!)

    View Slide

  39. Что НЕ хешируемо?
    39
    Lists, dicts, sets, tuples с изменяемыми объектами:

    View Slide

  40. Как работает dict
    40
    Представим my_dict = {}
    bucket offset (key, value) pairs references
    000
    001
    010
    011
    100
    101
    110
    111

    View Slide

  41. Как работает dict
    41
    Представим my_dict = {}
    Мы хотим сделать следующее:
    bucket offset (key, value) pairs references
    000
    001
    010
    011
    100
    101
    110
    111
    my_dict['newkey'] = 1
    my_dict['privet'] = 2

    View Slide

  42. Как работает dict
    42
    Представим my_dict = {}
    bucket offset (key, value) pairs references
    000
    001
    010
    011
    100
    101
    110
    111
    >>> hash('newkey')
    1521937699834405394

    View Slide

  43. Как работает dict
    43
    Представим my_dict = {}
    bucket offset (key, value) pairs references
    000
    001
    010
    011
    100
    101
    110
    111
    >>> hash('newkey')
    1521937699834405394
    >>> bin(hash('newkey'))
    1010100011111000000100100011001000000111001111010001000010010

    View Slide

  44. Как работает dict
    44
    Представим my_dict = {}
    bucket offset (key, value) pairs references
    000
    001
    010
    011
    100
    101
    110
    111
    >>> hash('newkey')
    1521937699834405394
    >>> bin(hash('newkey'))
    1010100011111000000100100011001000000111001111010001000010010

    View Slide

  45. Как работает dict
    45
    Представим my_dict = {}
    bucket offset (key, value) pairs references
    000
    001
    010 (‘newkey’, 1)
    011
    100
    101
    110
    111
    >>> hash('newkey')
    1521937699834405394
    >>> bin(hash('newkey'))
    1010100011111000000100100011001000000111001111010001000010010

    View Slide

  46. Как работает dict
    46
    Представим my_dict = {}
    bucket offset (key, value) pairs references
    000
    001
    010 (‘newkey’, 1)
    011 (‘privet’, 2)
    100
    101
    110
    111
    >>> hash('newkey')
    1521937699834405394
    >>> bin(hash('newkey'))
    1010100011111000000100100011001000000111001111010001000010010
    >>> bin(hash('privet'))
    101000100111100111100111001010111111110100111000010110011

    View Slide

  47. Как работает dict, когда значений больше 8? J
    47
    — При загрузке более 2/3 словаря, происходит увеличение массива вдвое
    — Средняя загрузка колеблется между 1/3 и 2/3

    View Slide

  48. Коллизии
    48

    View Slide

  49. View Slide

  50. Chaining
    50

    View Slide

  51. Open addressing
    51
    В общем виде

    View Slide

  52. Open addressing
    52
    Удаление

    View Slide

  53. Chaining vs probing
    53
    Кстати говоря, про linear probing

    View Slide

  54. Open addressing
    54
    — Эффективнее по памяти (как ни странно)
    — Берет пробы приблизительно так:
    — Linear probing имеет кучу проблем (primary clustering, например). Поэтому существуют quadtratic
    probing и пр.
    — Все это крайне чувствительно к хеш функции (нужно равномерное распределение)
    probes[i] = hash(key) + i % number_of_buckets

    View Slide

  55. Но есть хорошая
    новость — в питоне
    PRNG, а не linear
    probing
    55

    View Slide

  56. Compact dict (>=3.6)
    56
    # было
    hash_table = [
    ('--', '--', '--'),
    (542403711206072985, 'two', 2),
    ('--', '--', '--'),
    (4677866115915370763, 'three', 3),
    ('--', '--', '--'),
    (-1182584047114089363, 'one', 1),
    ('--', '--', '--'),
    ('--', '--', '--')
    ]
    # стало
    hash_table = [None, 1, None, 2, None, 0, None, None]
    entries = [
    (-1182584047114089363, 'one', 1),
    (542403711206072985, 'two', 2),
    (4677866115915370763, 'three', 3),
    ]

    View Slide

  57. Compact dict (>=3.6)
    57

    View Slide

  58. Compact dict (>=3.6)
    58
    Теорема Халла-Добелла / Линейный конгруэнтный генератор

    View Slide

  59. View Slide

  60. View Slide

  61. /
    Спасибо
    61
    Денис Аникин
    https://xfenix.ru

    View Slide