0

I was using an map with a map<long, X> For tuning performance I decided to experiment with unordered map too. I tried a 100k inserts using the operator[] with both. The map took ~140seconds whereas the unordered map tool ~230 seconds for the same code. I was expecting the unordered_map to be much faster! Am I wrong or is something fishy here?

Have searched for previous answers but they all point to unordered_map being faster. Hence the question. Can provide more details. Ran the below benchmark, first case takes ~0 seconds, 2nd one takes 8 seconds.

#include <map>
#include <unordered_map>
#include <iostream>
#include <time.h>
#include <stdlib.h>

using namespace std;

struct X{
   long a, b, c, d, e, f;
   string x, y, z;
};

struct mapZ{
   long id;
   map<long, X> xMap;
};

struct unmapZ{
   long id;
   unordered_map<long, X> xUnMap;
};

unmapZ & addUnMap(unmapZ & um, X & xtmp)
{
    um.xUnMap[xtmp.a] = xtmp;
    return(um);
}

mapZ & addMap(mapZ & mp, X & xtmp)
{
    mp.xMap[xtmp.a] = xtmp;
    return(mp);
}

int main()
{
    int numItr = 10000;
    map<long, X> myMap;
    unordered_map<long, X> myUnMap;
    mapZ mp;
    unmapZ uz;
    uz.id = (long)1;
    uz.xUnMap = myUnMap;
    mp.id = (long)1;
    mp.xMap = myMap;

    time_t start = time(0);

    for(int i = 0; i < numItr; i++)
    {
        long id = (long)rand();
        X tmp;
        tmp.a = id; tmp.b = id; tmp.c = id; tmp.d = id; tmp.e=id; tmp.f=id;
        tmp.x = "ABCDEF"; tmp.y = "WXYZS"; tmp.z = "UVGHJ";
        mp = addMap(mp, tmp);
    }
    cout << "size of map: " << mp.xMap.size() << "\n";
    time_t eof_map = time(0);

    for(int i = 0; i < numItr; i++)
    {
        long id = (long)rand();
        X tmp;
        tmp.a = id; tmp.b = id; tmp.c = id; tmp.d = id; tmp.e=id; tmp.f=id;
        tmp.x = "ABCDEF"; tmp.y = "WXYZS"; tmp.z = "UVGHJ";
        uz = addUnMap(uz, tmp);
    }

    cout << "size of unmap: " << uz.xUnMap.size() << "\n";
    time_t eof_unmap = time(0);

    cout << "Map inset time: " << eof_map - start << "\n";
    cout << "Unord Map insert time: " << eof_unmap - eof_map << "\n";
    return(0);
   }

Here is the command line to run the benchmark:

g++ -O3 -std=c++0x -m64 -Wno-write-strings -fopenmp mapTest.cpp

running GCC 4.6 on Ubuntu.

Konrad Rudolph
  • 530,221
  • 131
  • 937
  • 1,214
Naveen Sharma
  • 1,057
  • 12
  • 32

2 Answers2

4

There are many, many problems with this benchmark code, rendering its output irrelevant.

Firstly, you've used a completely unreliable and terrible timing clock. Use std::high_performance_clock.

Secondly, you've padded the sample value types with many extra std::strings and you copy this type everywhere. The high variability of the memory allocator (and the fact that you unfairly ran one test from a non-fresh process) is very bad.

Thirdly, you've included things like I/O in the benchmark time- and no, it's still totally not fair at all when you output the same string.

Fourthly, rand() can only produce a small range of values, which is an unfair comparison for the unordered_map. If you have a use case that really is only a small range of values, you can get much better push out of a hash map with an appropriate hasher change.

But the whale here is that your test is unfair because you self-assigned. You assigned the map and unordered-map to themselves. This is an unfair test because the legacy map code has a self-assignment check in it making the assignment a no-op. The new unordered_map code follows the new best practices and doesn't. Effectively, you redundantly copied the unordered_map and only the unordered_map thousands of extra times just for fun, changing O(n) into O(n^2) or O(n^3). Of course nobody with any sanity self-assigns, so this completely blows all the results out of any kind of relevance.

Puppy
  • 144,682
  • 38
  • 256
  • 465
  • ok, i can correct 1,2,3,4 but don't understand the self-assign part. do you mean when I'm adding to the map and returning the same object? I thought that might be safer as passing by reference would reduce copying of variables. – Naveen Sharma Mar 05 '14 at 17:28
  • You do `uz = addUnMap(...)`. That's assigning `uz` to the reference you just returned. – Puppy Mar 05 '14 at 17:37
  • so should i be returning the value i.e change the `addUnMapZ` function to `unMapZ addUnMapZ()`. relatively new to c++, so thanks for your patience – Naveen Sharma Mar 05 '14 at 17:45
  • No, simply don't assign the return value. – Puppy Mar 05 '14 at 17:58
  • yes, i think i got it. add functions should be void. It reduces the time taken by factor of 1000. can you pls confirm if thats right. so it should be `void addUnMapZ(unMapZ & um...` and in main `addUnMapZ(uz,..` – Naveen Sharma Mar 05 '14 at 18:03
  • Yes, that's about right. This fix accounts for most of the problem. – Puppy Mar 05 '14 at 18:07
  • self assignment - "new best practices" - where can I read about this? – Karoly Horvath Mar 06 '14 at 09:06
3

It depends on which values exactly your keys take, and which lookup patterns you are using. However, on more or less evenly spread keys with random access usage, std::unordered_map lookup and insertion should be quite a bit faster than std::map.

I can reproduce your results on GCC 4.8 but not on Clang 3.4. The differences are sufficiently large that even a skewed benchmark shouldn’t make that much of a difference: std::unordered_map is orders of magnitude slower than std::map. This could be a bug in the GCC c++stdlib implementation, or in g++’ optimiser – although it might still be an artefact of the benchmark – a better microbenchmark tool to assess these differences is Nonius.

Konrad Rudolph
  • 530,221
  • 131
  • 937
  • 1,214
  • Am not doing any lookup as of now. just testing how much time adding 100k elements take. just 100k calls to `map[X.id] = X;`There might be duplicate keys though in the 100k Xs. – Naveen Sharma Mar 05 '14 at 13:48
  • 1
    @NaveenSharma Every insertion is also a lookup. And your comment does **not** give enough details. Post the complete benchmark code. – Konrad Rudolph Mar 05 '14 at 13:54
  • can it be an issue of passing by address `&`. – Naveen Sharma Mar 05 '14 at 15:35
  • @NaveenSharma No, because you’re not passing by address, you’re passing by reference. You are not compiling with optimisations enabled, though. This invalidates the benchmark. On my machine, both tests finish immediately. In addition, you are benchmarking tons of other operations besides the insertion – this will skew the benchmark sufficiently to invalidate it entirely. Nevertheless, if I increase the iterations fourfold, `unordered_map` is twice as fast as `map`. – Konrad Rudolph Mar 05 '14 at 16:06
  • I compiled with `g++ -O3 -std=c++0x -m64 -Wno-write-strings -fopenmp mapTest.cpp`. should i be including anything else? – Naveen Sharma Mar 05 '14 at 16:09
  • I am on ubuntu and using gcc 4.6 – Naveen Sharma Mar 05 '14 at 16:10
  • @NaveenSharma Okay, now my curiosity is piqued. See updated answer. Just a small comment: GCC 4.6 is terribly outdated – upgrade your compiler. – Konrad Rudolph Mar 05 '14 at 16:20
  • upgrading compiler but should i be using any other optimization flags? – Naveen Sharma Mar 05 '14 at 16:21
  • @NaveenSharma `-O3` is fine. The rest isn’t really (that) relevant here. – Konrad Rudolph Mar 05 '14 at 16:26
  • 1
    Are you executing this program on an abacus? – Lightness Races in Orbit Mar 05 '14 at 16:43
  • @KonradRudolph, upgraded both gcc and g++ to 4.8.1. ran the code again. Map still runs in almost no time, unordere_map now takes 17 seconds(double of gcc 4.6) – Naveen Sharma Mar 05 '14 at 17:03