4

I am working on a low latency application which needs to be highly efficient all the time.

I need to lookup some index based on string, so I am using c++ unordered_map. Constraints: -Only insertions and lookups, no removals -key is string, value is int -Expecting no more than 1 million entries to be added to unordered_map

  • I am setting the unordered_map reserve to 1 million, Is this good or should I reserve by an order of some % more than expected entries to avoid rehashing? Can I set it to 1million or should I set to a large prime number close to 1million or some 2 power.

  • I am using default string hash function in c++ std lib which happens to be murmur2. My keys are between - 25 to 50 characters and all are unique keys containing numbers, upper case english alphabet and _ characters. Would this hash function suffice to distribute keys evenly or do I need to supply a better hash function to the unordered_map?

  • Would the unordered_map allocate space for 1 million key, value pairs and also an array of size 1 million when i call reserve or at reserve only the array of that size gets created and the key,value pairs gets allocated dynamically on insertion?

  • How much drag would be the dynamic allocation of key,value pairs on the heap on insertion? Especially since this is a big hash table with many entries.

  • For performance reasons, would it be good idea to implement my own hash table with memory pre-allocated for 1 million entries on stack or during init, or the above optimizations of unordered_map is close enough?

  • Is there a way to allocate the memory in advance for expected number of entries in unorderd_map in advance to avoid dynamic allocation on insertion?

Medicine
  • 1,923
  • 2
  • 23
  • 33

1 Answers1

1

Let's try to answer some of these questions with code. I'm not pasting the whole thing, since it's a bit long. Please find all the code here. I paste part of the output here though:

Map without reserve

        size: 0
bucket_count: 23
 load_factor: 0

Allocation count: 0

... 
about 15 reallocations deleted 
...

Allocation count: 1000015

        size: 1000000
bucket_count: 1236397
 load_factor: 0.808802

0: 550454
1: 445645
2: 180174
3: 48593
4: 9708
5: 1568
6: 231
7: 22
8: 2

Map with reserve

        size: 0
bucket_count: 23
 load_factor: 0

Allocation count: 1

        size: 0
bucket_count: 2144977
 load_factor: 0

Allocation count: 1000000

        size: 1000000
bucket_count: 2144977
 load_factor: 0.466205

0: 1346008
1: 626748
2: 146625
3: 22663
4: 2669
5: 248
6: 15
7: 1
  • As you see, when you reserve space for 1m elements, only one allocation happens. That is for the buckets, I guess.
  • The number of buckets reserved is much higher than 1m.
  • The number of allocation is exactly the same as number of elements inserted.
  • The hash distribution you can see for each case: there's quite of bunch of collisions. Sometimes up to 8 elements per bucket, even though there's half a million of buckets are empty.
  • Without initial reserve there's about 15 reallocation along the way, but the resulting map has fewer buckets.
  • With the big enough reserve there are no reallocations at all.
  • For sure, you could roll your own hash table. For example, you could reserve one contiguous block of space for all the keys, since they are no longer than 50 bytes each and and a block for values. But I'm sure that would be quite some work, possibly without good benefits. Profile and log your memory allocations before you start reimplementing what might not have to be.
detunized
  • 15,059
  • 3
  • 48
  • 64
  • if i set load factor to 1.0, buckets should be equal to entries on reserve, correct? – Medicine Oct 21 '13 at 20:05
  • Can you do that? I believe you can only change `max_load_factor` and it's at 1.0 by default. – detunized Oct 21 '13 at 22:29
  • Sorry, I meant, when max_load_factor is set to 1.0 which is also default, then buckets and elements count should be same? Also, when reserve is set to 1 million elements, space for corresponding number of buckets get allocated or space for both buckets and elements(key,value) pairs gets allocated? Dynamic memory allocation is one of NOs in writing low latency applications. – Medicine Oct 21 '13 at 22:48
  • @Medicine, I looked at the GCC's `unordered_map` implementation and it multiplies number of buckets by `_S_growth_factor` (which is `2`) inside `hashtable_policy.h` and then takes the smallest prime number bigger than the result from the supplied list `__prime_list`. So this is where `2144977` comes from. – detunized Oct 22 '13 at 11:12
  • @Medicine, by supplying your own allocator you can control the memory allocation operations. It would make sense to preallocate a pool bucket nodes and just allocate from there. – detunized Oct 22 '13 at 11:52