1

I've been using std::vector mostly and was wondering if I should use std::map for a key lookup to improve performance.

And here's my full test code.

#include <iostream>
#include <string>
#include <map>
#include <vector>
#include <ctime>
#include <chrono>

using namespace std;

vector<string> myStrings = {"aaa", "bbb", "ccc", "ddd", "eee", "fff", "ggg", "hhh", "iii", "jjj", "kkk", "lll", "mmm", "nnn", "ooo", "ppp", "qqq", "rrr", "sss", "ttt", "uuu", "vvv", "www", "xxx", "yyy", "zzz"};

struct MyData {

    string key;
    int value;
};

int findStringPosFromVec(const vector<MyData> &myVec, const string &str) {

    auto it = std::find_if(begin(myVec), end(myVec),
                           [&str](const MyData& data){return data.key == str;});
    if (it == end(myVec))
        return -1;
    return static_cast<int>(it - begin(myVec));
}

int main(int argc, const char * argv[]) {

    const int testInstance = 10000; //HOW MANY TIMES TO PERFORM THE TEST

    //----------------------------std::map-------------------------------
    clock_t map_cputime = std::clock(); //START MEASURING THE CPU TIME

    for (int i=0; i<testInstance; ++i) {

        map<string, int> myMap;

        //insert unique keys
        for (int i=0; i<myStrings.size(); ++i) {

            myMap[myStrings[i]] = i;
        }
        //iterate again, if key exists, replace value;
        for (int i=0; i<myStrings.size(); ++i) {

            if (myMap.find(myStrings[i]) != myMap.end())
                myMap[myStrings[i]] = i * 100;
        }
    }
    //FINISH MEASURING THE CPU TIME
    double map_cpu = (std::clock() - map_cputime) / (double)CLOCKS_PER_SEC;
    cout << "Map Finished in " << map_cpu << " seconds [CPU Clock] " << endl;


    //----------------------------std::vector-------------------------------
    clock_t vec_cputime = std::clock(); //START MEASURING THE CPU TIME

    for (int i=0; i<testInstance; ++i) {

        vector<MyData> myVec;

        //insert unique keys
        for (int i=0; i<myStrings.size(); ++i) {

            const int pos = findStringPosFromVec(myVec, myStrings[i]);

            if (pos == -1)
                myVec.push_back({myStrings[i], i});
        }
        //iterate again, if key exists, replace value;
        for (int i=0; i<myStrings.size(); ++i) {

            const int pos = findStringPosFromVec(myVec, myStrings[i]);

            if (pos != -1)
                myVec[pos].value = i * 100;
        }
    }
    //FINISH MEASURING THE CPU TIME
    double vec_cpu = (std::clock() - vec_cputime) / (double)CLOCKS_PER_SEC;
    cout << "Vector Finished in " << vec_cpu << " seconds [CPU Clock] " << endl;
    return 0;
}

And this is the result I got.

Map Finished in 0.38121 seconds [CPU Clock] 
Vector Finished in 0.346863 seconds [CPU Clock] 
Program ended with exit code: 0

I mostly store less than 30 elements in a container.

Does this mean it is better to use std::vector instead of std::map in my case?

EDIT: when I move map<string, int> myMap; before the loop, std::map was faster than std::vector.

Map Finished in 0.278136 seconds [CPU Clock] 
Vector Finished in 0.328548 seconds [CPU Clock] 
Program ended with exit code: 0

So If this is the proper test, I guess std::map is faster.

But, If I reduce the amount of elements to 10, std::vector was faster so I guess it really depends on the number of elements.

Zack Lee
  • 2,784
  • 6
  • 35
  • 77
  • 3
    Your map timing includes populating the map multiple times. The vector doesn't. To get a realistic compare, populate the map once outside the timing code. – John3136 Nov 09 '17 at 02:02
  • Use a bench marking library, you aren't accounting for things like cache heating. – user975989 Nov 09 '17 at 02:04
  • You are comparing linear-time lookup to logarithmic time lookup. Dude c'mon. Use the right data-structure and you won't be comparing apples to peanuts – smac89 Nov 09 '17 at 02:23
  • You may already know this, but `unordered_map` and `unordered_set` are much faster than either of them for lookups any time it is likely to matter (with large datasets). – zzxyz Nov 09 '17 at 02:29

2 Answers2

8

I would say that in general, it's possible that a vector performs better than a map for lookups, but for a tiny amount of data only, e.g. you've mentioned less than 30 elements.

The reason is that linear search through continuous memory chunk is the cheapest way to access memory. A map keeps data at random memory locations, so it's a little bit more expensive to access them. In case of a tiny number of elements, this might play a role. In real life with hundreds and thousands of elements, algorithmic complexity of a lookup operation will dominate this performance gain.

BUT! You are benchmarking completely different things:

  1. You are populating a map. In case of a vector, you don't do this
  2. Your code could perform TWO map lookups: first, find to check existence, second [] operator to find an element to modify. These are relatively heavy operations. You can modify an element just with single find (figure this out yourself, check references!)
  3. Within each test iteration, you are performing additional heavy operations, like memory allocation for each map/vector. It means that your tests are measuring not only lookup performance but something else.
  4. Benchmarking is a difficult problem, don't do this yourself. For example, there are side effects like cache heating and you have to deal with them. Use something like Celero, hayai or google benchmark
CaptainTrunky
  • 1,562
  • 2
  • 15
  • 23
  • Speaking only on the first two paragraphs: Yes, and the reason in this case the memory is contiguous is that the vector is holding `std::string` for values which very likely fit in the library's "small string optimization" range - thus the entire string is in contiguous memory. Given the answer [here](https://stackoverflow.com/a/21710033/751579) plus a typical [64 byte cache line width](http://www.7-cpu.com/cpu/Haswell.html) this vector of strings fits in 10 cache lines - accessed sequentially hence prefetched. The map is all over the place. This vector search compares better cold than hot! – davidbak Nov 09 '17 at 02:30
  • Let me clarify: It also well work better hot than the map, just with less of an advantage once the map - also small - is all brought into cache. – davidbak Nov 09 '17 at 02:30
  • @davidbak Nice addition! I deliberately haven't mention caches and related topics to keep it simple. – CaptainTrunky Nov 09 '17 at 02:33
2

Your vector has constant content, so the compiler optimizes most of your code away anyway.
There is little use in measuring for such small counts, and no use measuring for hard coded values.

Aganju
  • 6,295
  • 1
  • 12
  • 23