Подсчет коллизий в словаре Python
моя первая публикация здесь, поэтому надеюсь, что я задал свой вопрос правильным образом,
После добавления элемента в словарь 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 столкновений.
Кто-нибудь знает, почему это говорит о столкновении?
Edit:
Возможно, я понял это.
Это потому, что __eq__
вызывается только в том случае, если значения __hash__
для двух объектов одинаковы, а не для их "хрусткой версии" (как выразился @John Machin)?
Ответы
Ответ 1
Короткий ответ:
Вы не можете имитировать использование идентификаторов объектов в виде ключей dict, используя случайные целые числа в качестве ключей dict. Они имеют разные хэш-функции.
Столкновения происходят. "Наличие уникальных штучек означает отсутствие столкновений" неверно для нескольких значений "thingy".
Вы не должны беспокоиться о столкновениях.
Длинный ответ:
Некоторые объяснения, полученные из чтение исходного кода:
A dict реализуется как таблица из 2 ** я записей, где я - целое число.
dicts не превышает 2/3. Следовательно, для 15000 ключей я должен быть 15 и 2 ** я - 32768.
Если o - произвольный экземпляр класса, который не определяет __hash__()
, это НЕ верно, что hash (o) == id (o). Поскольку адрес, вероятно, имеет нули в младших 3 или 4 битах, хэш создается путем поворота адреса на 4 бита; см. исходный файл Objects/object.c, function _Py_HashPointer
Было бы проблемой, если бы было много нулей в младших битах, потому что для доступа к таблице размера 2 ** я (например, 32768) хеш-значение (часто намного больше) должно быть хрустело чтобы соответствовать, и это делается очень просто и быстро, взяв младший бит я (например, 15) хэш-значения.
Следовательно, столкновения неизбежны.
Однако это не вызывает паники. Остальные биты хэш-значения учитываются при расчете того, где будет следующий пробник. Вероятность необходимости 3-го и т.д. Зонда должна быть довольно небольшой, тем более, что дикты не превышают 2/3. Стоимость нескольких зондов снижается за счет дешевой стоимости расчета слота для первого и последующих зондов.
Ниже приведен простой пример, иллюстрирующий большую часть приведенного выше обсуждения. Он предполагает случайный доступ к dict после того, как он достиг своего максимального размера. С Python2.7.1 он показывает около 2000 столкновений для 15000 объектов (13.3%).
В любом случае суть в том, что вы должны действительно отвлечь свое внимание в другом месте. Коллизии не являются вашей проблемой, если вы не достигли чрезвычайно необычного способа получения памяти для своих объектов. Вы должны посмотреть, как вы используете дикты, например. используйте k in d
или try/except, а не d.has_key(k)
. Рассмотрим, что один диск получил доступ как d[(x, y)]
вместо двух уровней, доступных как d[x][y]
. Если вам нужна помощь, задайте отдельный вопрос.
Обновить после тестирования на Python 2.6:
Вращение адреса не было введено до Python 2.7; см. этот отчет об ошибках для всестороннего обсуждения и тестов. Основные выводы IMHO по-прежнему действительны и могут быть дополнены "Обновить, если вы можете".
>>> 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
>>>
Ответ 2
Эта идея на самом деле не работает, см. обсуждение в вопросе.
Быстрый просмотр реализации C на python показывает, что код для разрешения конфликтов не вычисляет или не сохраняет количество столкновений.
Однако он будет вызывать PyObject_RichCompareBool
на клавишах, чтобы проверить, совпадают ли они. Это означает, что __eq__
на ключе будет вызываться для каждого столкновения.
Итак:
Замените свои ключи объектами, которые определяют __eq__
и увеличивают счетчик при его вызове. Это будет медленнее из-за накладных расходов, связанных с переходом на python для сравнения. Однако это должно дать вам представление о том, сколько коллизий происходит.
Убедитесь, что в качестве ключа используются разные объекты, иначе python будет использовать ярлык, потому что объект всегда равен самому себе. Кроме того, убедитесь, что хэш объектов имеет то же значение, что и исходные ключи.
Ответ 3
Если ваши ключи гарантированно являются уникальными целыми числами, а так как Python использует hash()
на клавишах, то вам не должно быть никаких коллизий.