Подсчет коллизий в словаре Python - PullRequest
15 голосов
/ 01 февраля 2011

я впервые здесь пишу, так что надеюсь, что я правильно задал вопрос,

После добавления элемента в словарь Python можно ли заставить Python сообщить вам, вызвало ли добавление этого элемента конфликт? (И сколько локаций использовала стратегия разрешения столкновений, прежде чем найти место для размещения элемента?)

Моя проблема: я использую словари как часть более крупного проекта, и после обширного профилирования я обнаружил, что самая медленная часть кода имеет дело с матрицей разреженного расстояния, реализованной с использованием словарей.

Ключи, которые я использую, являются идентификаторами объектов Python, которые являются уникальными целыми числами, поэтому я знаю, что все они хэшируются с разными значениями. Но размещение их в словаре может привести к коллизиям в принципе. Я не верю, что словарные коллизии замедляют мою программу, но я хочу исключить их из своих запросов.

Так, например, дан следующий словарь:

d = {}
for i in xrange(15000):
    d[random.randint(15000000, 18000000)] = 0

можете ли вы заставить Python сообщить, сколько коллизий произошло при его создании?

Мой настоящий код запутался в приложении, но приведенный выше код создает словарь, который очень похож на те, которые я использую.

Повторюсь: я не думаю, что коллизии - это то, что замедляет мой код, я просто хочу исключить возможность, показав, что в моих словарях не так много коллизий.

Спасибо за вашу помощь.

Редактировать: Некоторый код для реализации решения @Winston Ewert:

n = 1500
global collision_count
collision_count = 0

class Foo():

    def __eq__(self, other):
        global collision_count
        collision_count += 1
        return id(self) == id(other)

    def __hash__(self):
        #return id(self) # @John Machin: yes, I know!
        return 1

objects = [Foo() for i in xrange(n)]

d = {}
for o in objects:
    d[o] = 1

print collision_count

Обратите внимание, что когда вы определяете __eq__ для класса, Python дает вам TypeError: unhashable instance, если вы также не определяете функцию __hash__.

Он работает не совсем так, как я ожидал. Если у вас есть функция __hash__ return 1, то вы получите множество коллизий, как и ожидалось (1125560 коллизий для n = 1500 в моей системе). Но с return id(self) происходит 0 столкновений.

Кто-нибудь знает, почему это говорит о 0 столкновениях?

Edit: Я мог бы понять это.

Это потому, что __eq__ вызывается только в том случае, если значения __hash__ двух объектов одинаковы, а не их "сжатая версия" (как сказал @Джон Мачин)?

Ответы [ 3 ]

9 голосов
/ 01 февраля 2011

Краткий ответ:

Вы не можете моделировать использование идентификаторов объектов в качестве ключей dict, используя случайные целые числа в качестве ключей dict.У них разные хеш-функции.

Столкновения случаются.«Наличие уникальных штуковин означает отсутствие столкновений» неправильно для нескольких значений «штуковин».

Вам не следует беспокоиться о столкновениях.

Длинный ответ:

Некоторые пояснения, полученные из чтения исходного кода :

Диктовка реализована в виде таблицы из 2 ** i записей, где i - целое число.

диктов не более 2/3.Следовательно, для 15000 ключей i должно быть 15, а 2 ** i - 32768.

Когда o - произвольный экземпляр класса, который не определяет __hash__(), , это НЕ верно, что хеш(o) == id (o) .Поскольку адрес, вероятно, будет иметь нули в младших 3 или 4 битах, хеш создается путем поворота адреса вправо на 4 бита;см. исходный файл Objects / object.c , функция _Py_HashPointer

Было бы проблемой, если бы было много нулей в младших разрядах, потому что для доступа к таблицеразмер 2 ** i (например, 32768), хеш-значение (часто намного больше этого) должно быть сокращено, чтобы соответствовать, и это делается очень просто и быстро, беря младший бит i (например, 15) битового значения.

Следовательно, столкновения неизбежны .

Однако это не повод для паники.Остальные биты значения хэша учитываются при расчете того, где будет следующий зонд.Вероятность необходимости 3-го зонда и т. Д. Должна быть довольно малой, тем более что полнота никогда не бывает более 2/3 полной.Стоимость нескольких зондов снижается за счет дешевой стоимости расчета слота для первого и последующих зондов.

Приведенный ниже код представляет собой простой эксперимент, иллюстрирующий большую часть вышеприведенного обсуждения.Это предполагает случайный доступ к dict после того, как он достиг своего максимального размера.В Python2.7.1 он показывает около 2000 коллизий для 15000 объектов (13,3%).

В любом случае, суть в том, что вы действительно должны отвлечь свое внимание в другом месте.Столкновения не являются вашей проблемой, если вы не достигли какого-то чрезвычайно ненормального способа получить память для ваших объектов.Вы должны посмотреть, как вы используете диктовку, например, использовать k in d или попробовать / исключить, а не d.has_key(k).Рассмотрим один диктовку, к которой обращаются d[(x, y)] вместо двух уровней, к которым обращаются d[x][y]Если вам нужна помощь в этом, задайте отдельный вопрос.

Обновление после тестирования на Python 2.6:

Поворот адреса не был введен до Python 2.7;см. этот отчет об ошибках для всестороннего обсуждения и тестов производительности.Основные выводы ИМХО по-прежнему действительны и могут быть дополнены "Обновить, если можете".

>>> n = 15000
>>> i = 0
>>> while 2 ** i / 1.5 < n:
...    i += 1
...
>>> print i, 2 ** i, int(2 ** i / 1.5)
15 32768 21845
>>> probe_mask = 2 ** i - 1
>>> print hex(probe_mask)
0x7fff
>>> class Foo(object):
...     pass
...
>>> olist = [Foo() for j in xrange(n)]
>>> hashes = [hash(o) for o in olist]
>>> print len(set(hashes))
15000
>>> probes = [h & probe_mask for h in hashes]
>>> print len(set(probes))
12997
>>>
5 голосов
/ 01 февраля 2011

Эта идея на самом деле не работает, см. Обсуждение в вопросе.

Беглый взгляд на реализацию Python для C показывает, что код для разрешения коллизий не вычисляет и не сохраняет количество коллизий.

Однако он вызовет PyObject_RichCompareBool на клавишах, чтобы проверить, совпадают ли они. Это означает, что __eq__ для ключа будет вызываться для каждого столкновения.

Итак:

Замените ваши ключи объектами, которые определяют __eq__ и увеличивают счетчик при его вызове. Это будет медленнее из-за накладных расходов, связанных с переходом на python для сравнения. Тем не менее, это должно дать вам представление о том, сколько происходит столкновений.

Убедитесь, что вы используете разные объекты в качестве ключа, иначе Python будет использовать ярлык, потому что объект всегда равен самому себе. Также убедитесь, что хэш объектов имеет то же значение, что и исходные ключи.

0 голосов
/ 01 февраля 2011

Если ваши ключи гарантированно являются уникальными целыми числами, и поскольку Python использует hash() для ключей, то вам гарантированно не будет никаких коллизий.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...