2

There are a few items to iterate and search by the key. And I already form a std::vector for iteration. Do I need to form a struct for searching such as std::unordered_map?

I do know searching in std::vector resulted in O(N) and searching in std::unordered_map resulted in O(1). But the items inside are about 10. No insert or update happened after initialization. I may search for many times. Maybe 1 millions, 1 billions, or even more, I can't make sure of it.

I am concerned that hashing may be costlier than iteration.

Here is a sample:

class Item
{
public:
    int key;
    const char* value;
};

class Items
{
public:
    Items(const std::vector<const Item> items) 
    : _vector(items)
    , _map(generateMap()){
    }

    const char* getValueByKey(int key) const {
        //which one to choose
        //map
//        const auto& iter = _map.find(key);
//        if (iter!=_map.end()) {
//            return iter->second;
//        }
//        return nullptr;
        //vector
        for (const auto& iter : _vector) {
            if (iter.key==key) {
                return iter.value;
            }
        }
        return nullptr;
    }

protected:
    const std::unordered_map<int, const char*> generateMap() const{
        std::unordered_map<int, const char*> map;
        for (const auto& item : _vector) {
            map.insert({item.key, item.value});//I can make sure that no same key will exists
        }
        return map;
    }

    const std::vector<const Item> _vector;
    const std::unordered_map<int, const char*> _map;//Is it necessary?
};

int main() 
{   
    const std::vector<const Item> items ={
        {1, "value_1"},
        {20, "value_2"},
        {10, "value_3"},
        {55, "value_4"},
    }; 
    Items theItems = items;
    srand(time(nullptr));
    for (int i = 0; i < 1000000; i++) {
        int key = rand();
        printf("%d %s exists\n", key, theItems.getValueByKey(key)==nullptr?"is not":"is");
    }
    return 0;
}

Here is an int key case, maybe no hashing happened. But what about other cases, a std::string, a user-defined struct and so on?

So how should I make my decision for this kind of case theoretically?

jotik
  • 17,044
  • 13
  • 58
  • 123
Ringo_D
  • 784
  • 5
  • 18
  • 4
    benchmark them, don't guess. But only benchmark them once you've actually written a program and determined that this is a bit of code that actually has significant impact on your overall performance. – xaxxon Feb 09 '17 at 08:08
  • Why do you exclude the *ordered* `std::map` option? It's a good compromise in many situations. – A.S.H Feb 09 '17 at 08:27
  • @A.S.H Why `std::map` will be better? – Ringo_D Feb 09 '17 at 08:41
  • @xaxxon I am finding a **theoretical** answer . – Ringo_D Feb 09 '17 at 08:42
  • I'm not saying it would be better, it's an option as well and I wondered why you excluded it. It's a good option and has O(log n) for search time. – A.S.H Feb 09 '17 at 08:46
  • What kind of values are you storing in your vector ? If you can make 1:1 mapping between the values and the indexes, may be you can get a perfect O(1) search without unordered_map. BTW unordered_map is amortized O(1) not O(1). – Shasha99 Feb 09 '17 at 09:18
  • 1
    There is no theoretical answer based on the limited informatino you've provided. – xaxxon Feb 09 '17 at 09:46

4 Answers4

6

The politically correct answer is "benchmark!".

But based on the experience of others, when only using a small number of items of relatively small size, using a std::vector is usually faster (especially if its sorted), because it improves memory locality of your items and does not use extra heap allocations/deallocations for its items. However, if the key is something like a std::string and key-comparisons are done using its contents, then this of course might hurt memory-locality, because the string contents are not (always) contained in the string object itself, but on the heap.

jotik
  • 17,044
  • 13
  • 58
  • 123
4

If you are not going to change your data and you need to do many searches I suggest you to try to use a std:vector and sort it. Then you can use a find algorithm such as binary_search, lower_bound or upper_bound from the STL exploiting the fact that the container is sorted.

You get the best: both locality and O(log(N)) complexity.

jimifiki
  • 5,377
  • 2
  • 34
  • 60
  • 2
    Binary search is useless in this case. Why talking about O-notation when you can see it is at max O(10) i.e. O(1). – Shasha99 Feb 09 '17 at 09:20
  • @Shasha99 thank you for stressing the fact that in the specific case described in the question everything is O(1). Does it mean that locality matters more than anything else? If I talked about O-notation it was for the sake of generality. I hope that my question is going to be read by many people and I hope it can inspire someone of them. Using a map for finding elements even in circumstances where a sorted vector can do the job better is a common mistake I have done in the past. I hope readers can speculate about this fact. – jimifiki Feb 09 '17 at 09:50
0

In the case of a small number of items, but with a lookup on the order of a billion times, we need to see which is faster, a short iteration of a vector vs an unordered_map, which as stated above can give you O(1) performance as long as you avoid collisions. A single iteration of a vector is likely to be faster than the hashing of a map. The question then becomes how many items does the map become faster for an average lookup. To determine that answer you should perform bench-marking between the two to see what actually gives the best time for your particular situation.

Alternatively, because you mention no insert or update will happen after initialization, if the range of your keys is small you can use a lookup table which would give you the fastest performance (no hashing issues) at the cost of slight memory overhead.

Dave S
  • 973
  • 9
  • 17
  • 1
    O(1) doesn't mean fast. – xaxxon Feb 09 '17 at 09:47
  • That is why I mentioned the need to perform bench marking and that was my point in mentioning a lookup table in this situation when the number of items is small. – Dave S Feb 09 '17 at 09:52
  • Isn't a "lookup table" for arbitrary data a map - for which there are no performant data structures in std::? – xaxxon Feb 09 '17 at 09:54
  • No, you can have an array of the given structure and the index be the key – Dave S Feb 09 '17 at 09:57
  • If your keys are sequential numbers (or at least very dense) that can work, but I don't see anything in the question stating that condition. Oh I see you said " if the range of your keys is small " now. But for the range of numbers in his sample, it's not a good choice. You can scan the values faster than you can do a main memory load. – xaxxon Feb 09 '17 at 09:59
  • @xaxxon I mentioned that there may be a slight memory overhead. He may have only 10 keys that range from 1 to 50. Its a decision of memory verses performance. If he doing a very speed critical task, calling the function 10 billion times, perhaps the speed improvement is worth the modest waste of memory. – Dave S Feb 09 '17 at 10:04
  • "wasting memory" is a speed issue. There is a direct correspondence between tight memory and fast execution. – xaxxon Feb 09 '17 at 10:05
  • @xaxxon - you are paritially correct. It depends on HOW MUCH memory. If he has 10 objects that are not large memory wise there may be no penalty. On the otherhand, if the objects are large, so memory paging is required to access all of the objects in the table then yes. I was working on his statement that he only has a few objects and they were only a few bytes big. – Dave S Feb 09 '17 at 10:09
  • If there is a 55-byte range of keys, you are going from a single cache line to multiple cache lines. – xaxxon Feb 09 '17 at 10:11
  • That is correct given a 64-byte cache line. Again, he said specifically he is doing with few items so its possible the lookup may fit. But even it is bigger, the impact may not be slower than using vector/map. The only way to know for sure is to bench-mark on the target machine with realistic data. – Dave S Feb 09 '17 at 10:22
  • @xaxxon - do you really think going to multiple cache lines in a lookup table is going to be more costly then iterating across a vector or performing hashing to get the desired data? – Dave S Feb 09 '17 at 10:31
  • If you get into a situation where you are thrashing cache lines, then it can make a big difference not having to go back and forth between lines to get results based on which key is being looked up. – xaxxon Feb 09 '17 at 10:33
  • @xaxxon - If you have a L1 cache of size 32KB and the entire lookup is there but in several cache lines. The time to switch cache lines is pretty minimal (on the order of several clock cycles) as opposed to cache thrashing where you keep accessing general RAM which can be 10X slower.. – Dave S Feb 09 '17 at 11:11
  • Your L1 cache is shared across a lot of things. The smaller your data size, the higher the chance it's already there. – xaxxon Feb 09 '17 at 11:13
  • @xaxxon - I think we can both agree, the factors such as the key distribution, structure size, and number of keys play a factor in performance. In cases when there are not so many gaps between keys, the number of elements are small and the structures are not heavy, a lookup may be the fastest way. However, the only way to confirm is to bench-mark vs the other methods. – Dave S Feb 09 '17 at 11:19
  • So, the correct answer is to tell them to benchmark it. All the other theorizing gives the wrong impression. – xaxxon Feb 09 '17 at 11:34
  • I think the other theorizing explains why in the end bench-marking is the only real way, but I think the motivation for the question is given the choice, before doing any testing what would intuitively be the best solution based on a few elements that will be accessed a huge number of times. BTW, from the question, I see no reason why the keys cannot be constrained to be successive numbers. This would optimize the memory size of the lookup table, if that would be the way to go. – Dave S Feb 09 '17 at 11:40
0

I would take a look at boost::flat_map, which provides a map interface over a vector implementation.

Regardless of the Big O complexity, the fact is that your hardware will perform much better with a vector than a map because of data locality and prefetching data from main memory.

Quoting Chandler Carruth, "Maps are an exercise in slowing down your code"

fwg
  • 1,018
  • 2
  • 10
  • 25