Skip to main content

Python Dictionary Implementation

·3 mins

Overview #

  1. CPython allocation memory to save dictionary, the initial table size is 8, entries are saved as <hash,key,value> in each slot(The slot content changed after Python 3.6).
  2. When a new key is added, python use i = hash(key) & mask where mask=table_size-1 to calculate which slot it should be placed. If the slot is occupied, CPython using a probing algorithm to find the empty slot to store new item.
  3. When 2/3 of the table is full, the table will be resized.
  4. When getting item from dictionary, both hash and key must be equal.

Resizing #

When elements size is below 50000, the table size will increase by a factor of 4 based on used slots. Otherwise, it will increase by a factor of 2. The dictionary size is always \(2^{n}\).

dict sizeresize when elements in dictnew table size
8632
3222128
12886512

Removing item from dictionary doesn’t lead to shrink table. The value of the item will marks as null but not empty. When looking up element in dictionary, it will keep probing once find this special mark. So deleting element from Python will not decrease the memory using. If you really want to do so, you can the items in the old dictionary to create a new one.

Probing #

CPython used a modified random probing algorithm to choose the empty slot. This algorithm can traval all of the slots in a pseudo random order.

The travel order can be calculated by this formula: j = ((5*j) + 1) mod 2**i, where j is slot index.

For example, if table size is 8, and the calculate slot index is 2, then the traversal order should be:

2 -> (5*2+1) mod 8 = 3 -> (5*3+1) mod 8 = 0 -> (5*0+1) mod 8 = 1 -> 6 -> 7 -> 4 -> 5 -> 2

CPython changed this formula by adding perturb and PERTURB_SHIFT variables, where perturb is hash value and PERTURB_SHIFT is 5. By adding PERTURB_SHIFT, the probe sequence depends on every bit in the hash code, and the collision probability is decreased. And perturb will eventually becomes to 0, this ensures that all of the slots will be checked.

j = (5*j) + 1 + perturb;
perturb >>= PERTURB_SHIFT;
j = j % 2**i

Dictionary improvement after 3.6 #

CPython 3.6 use a compact representation to save entries, and “The memory usage of the new dict() is between 20% and 25% smaller compared to Python 3.5”.

Compact Hash Table #

As mentioned before, entries saved in the form of <hash,key,value>. This will takes 3B on 64 bit machine. And no matter how much item is added into the dictionary, the memory usage is the same(3B*table_size).

After 3.6, CPython use two structure to save data. One is index, another is the real data.

For example, if the table size is 8, and there is an item in slot 1, the index looks like this:

[null, 0, null, null, null, null, null, null]

And the real data is:

| hash | key  | value |
| xxx1 | yyy1 | zzz1  |

0 represents the items index on real data. If another item is added in slot 3, the new index become this:

[null, 0, null, 1, null, null, null, null]

The real data become this:

| hash | key  | value |
| xxx1 | yyy1 | zzz1  |
| xxx2 | yyy2 | zzz2  |

This saves memory, especially when table load factor is low.

Ref #

  1. How are Python’s Built In Dictionaries Implemented
  2. cpython source code
  3. Is it possible to give a python dict an initial capacity (and is it useful)
  4. Python dictionary implementation