Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Counting collisions in a Python dictionary

my first time posting here, so hope I've asked my question in the right sort of way,

After adding an element to a Python dictionary, is it possible to get Python to tell you if adding that element caused a collision? (And how many locations the collision resolution strategy probed before finding a place to put the element?)

My problem is: I am using dictionaries as part of a larger project, and after extensive profiling, I have discovered that the slowest part of the code is dealing with a sparse distance matrix implemented using dictionaries.

The keys I'm using are IDs of Python objects, which are unique integers, so I know they all hash to different values. But putting them in a dictionary could still cause collisions in principle. I don't believe that dictionary collisions are the thing that's slowing my program down, but I want to eliminate them from my enquiries.

So, for example, given the following dictionary:

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

can you get Python to tell you how many collisions happened when creating it?

My actual code is tangled up with the application, but the above code makes a dictionary that looks very similar to the ones I am using.

To repeat: I don't think that collisions are what is slowing down my code, I just want to eliminate the possibility by showing that my dictionaries don't have many collisions.

Thanks for your help.

Edit: Some code to implement @Winston Ewert's solution:

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

Note that when you define __eq__ on a class, Python gives you a TypeError: unhashable instance if you don't also define a __hash__ function.

It doesn't run quite as I expected. If you have the __hash__ function return 1, then you get loads of collisions, as expected (1125560 collisions for n=1500 on my system). But with return id(self), there are 0 collisions.

Anyone know why this is saying 0 collisions?

Edit: I might have figured this out.

Is it because __eq__ is only called if the __hash__ values of two objects are the same, not their "crunched version" (as @John Machin put it)?

like image 294
Adam Nellis Avatar asked Feb 01 '11 16:02

Adam Nellis


People also ask

How does Python handle collisions in a dictionary?

Python dict uses open addressing to resolve hash collisions (see dictobject. c:296-297). In open addressing, hash collisions are resolved by probing (explained below) . The hash table is just a contiguous block of memory (like an array, so you can do O(1) lookup by index).

How do you count the number of occurrences of an element in a dictionary in python?

If you want to count the occurrences of each value in a Python dictionary, you can use the collections. Counter() function on the dictionary values. It returns the number of times each value occurs in the dictionary.

How does Python handle hash collision in a set?

Python uses a method called Open Addressing for handling collisions. It also resizes the hash tables when it reaches a certain size, but we won't discuss that aspect. Open Addressing definition from Wikipedia: In another strategy, called open addressing, all entry records are stored in the bucket array itself.


2 Answers

Short answer:

You can't simulate using object ids as dict keys by using random integers as dict keys. They have different hash functions.

Collisions do happen. "Having unique thingies means no collisions" is wrong for several values of "thingy".

You shouldn't be worrying about collisions.

Long answer:

Some explanations, derived from reading the source code:

A dict is implemented as a table of 2 ** i entries, where i is an integer.

dicts are no more than 2/3 full. Consequently for 15000 keys, i must be 15 and 2 ** i is 32768.

When o is an arbitrary instance of a class that doesn't define __hash__(), it is NOT true that hash(o) == id(o). As the address is likely to have zeroes in the low-order 3 or 4 bits, the hash is constructed by rotating the address right by 4 bits; see the source file Objects/object.c, function _Py_HashPointer

It would be a problem if there were lots of zeroes in the low-order bits, because to access a table of size 2 ** i (e.g. 32768), the hash value (often much larger than that) must be crunched to fit, and this is done very simply and quickly by taking the low order i (e.g. 15) bits of the hash value.

Consequently collisions are inevitable.

However this is not cause for panic. The remaining bits of the hash value are factored into the calculation of where the next probe will be. The likelihood of a 3rd etc probe being needed should be rather small, especially as the dict is never more than 2/3 full. The cost of multiple probes is mitigated by the cheap cost of calculating the slot for the first and subsequent probes.

The code below is a simple experiment illustrating most of the above discussion. It presumes random accesses of the dict after it has reached its maximum size. With Python2.7.1, it shows about 2000 collisions for 15000 objects (13.3%).

In any case the bottom line is that you should really divert your attention elsewhere. Collisions are not your problem unless you have achieved some extremely abnormal way of getting memory for your objects. You should look at how you are using the dicts e.g. use k in d or try/except, not d.has_key(k). Consider one dict accessed as d[(x, y)] instead of two levels accessed as d[x][y]. If you need help with that, ask a seperate question.

Update after testing on Python 2.6:

Rotating the address was not introduced until Python 2.7; see this bug report for comprehensive discussion and benchmarks. The basic conclusions are IMHO still valid, and can be augmented by "Update if you can".

>>> 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
>>>
like image 68
John Machin Avatar answered Sep 25 '22 03:09

John Machin


This idea doesn't actually work, see discussion in the question.

A quick look at the C implementation of python shows that the code for resolving collisions does not calculate or store the number of collisions.

However, it will invoke PyObject_RichCompareBool on the keys to check if they match. This means that __eq__ on the key will be invoked for every collision.

So:

Replace your keys with objects that define __eq__ and increment a counter when it is called. This will be slower because of the overhead involved in jumping into python for the compare. However, it should give you an idea of how many collisions are happening.

Make sure you use different objects as the key, otherwise python will take a shortcut because an object is always equal to itself. Also, make sure the objects hash to the same value as the original keys.

like image 44
Winston Ewert Avatar answered Sep 25 '22 03:09

Winston Ewert