1

In my code I'm storing parameters sets for interactions between different groups in a map. Currently at startup I add each structure (testvals in the code below) with the key created from joining the two group names into a single string.

string nKey = key1;
nKey += JOIN_STRING;
nKey += key2;

map< string, struct> mymap_string; 
mymap_string.insert( make_pair(nKey, testval ));

When it comes to looking up the data for two groups, I'm again creating that string and then using find on the map to retrieve my data.

string nKey = key1;
nKey += JOIN_STRING;
nKey += key2;

auto it = mymap_string.find( nKey );
if ( it != mymap_string.end() )
{
    struct vals= it->second;
}

In my code I'm creating the map once at startup but doing the lookup part millions of times. I'm wondering if there's a better way of doing this as string concatenation seems to be relatively expensive and find may not be the fastest way to search and compare strings?

My testing seems to show that strings are faster than using std::pair<string1, string2> as the key for the map. I've looked at map vs unordered_map but there doesn't seem to be much of a difference. unordered_map may be slightly faster when the number of keys is large.

Does anyone have any suggestion on what might be a better, quicker approach? Given the number of calls made to this, if I can make it significantly quicker I can save a lot of time. I don't mind if the insertion or setup isn't blindingly fast since it only happens once, but lookup is important. It would be better to use something standard that works on Windows and Linux.

Update:

OK so from the questions it seems that more background information is required.

testvals is a structure of doubles for the input parameters for the current model being used and the number of variables provided in it will vary with the model. But typically this is between 4-10 values. A typical set is show here:

typedef struct
{
    double m_temp_min;
    double m_temp_max;
    double m_liquid_content;
    double m_growth_rate;
    double m_alpha;
    double m_beta;
} testvals;

Key1 and Key2 are always strings that are passed from the programs core module, but the strings are user-defined, meaning they could be anything from "a" to "my_big_yellow_submarine_3".

The number of keys in the map will depend on the number of groups in the data. If there are only two groups for which interactions parameters need to be provided, then the map would only have 4 unique string keys: group1~~group1, group1~~group2, group2~~group1 and group2~~group2. Normally there are 3 or 4 group types in the map so the number of keys is usually in the number of tens. This size may be why I don't see much of a difference in map and unordered_map performance.

One of the comments mentioned std::pair<std::string,std::string> and as I originally said, the cost of calling make_pair() seems much higher than the cost of making the string and was more than 50% slower when I tested it. But I didn't try the combination of std::pair with unordered_map. I assumed that if std::pair is slower with map, it is also going to be slower with unordered_map. Is there a reason to expect it to be very different?

I hope this helps clarify some of the things.

phuclv
  • 37,963
  • 15
  • 156
  • 475
jpmorr
  • 500
  • 5
  • 25
  • Please post the definition of `testval`'s struct type. How large is the `struct`? Are you storing it by-value inside the map or are you storing pointers/references? What kinds of values do `key1` and `key2` take, exactly? – Dai Nov 27 '20 at 00:11
  • 1
    I would try to define a custom type for the key, and a strict weak ordering comparator in order to effectively use two discrete strings as a key. For extra credit the key would include a `std::variant` containing a `std::reference_wrapper`, so that a temporary key for lookup purposes can be constructed without copying strings around. – Sam Varshavchik Nov 27 '20 at 00:13
  • 2
    `unordered_map` will be faster than `map` because the internal representation is a hashtable for `O(1)` lookup (assuming sufficient capacity and load-factor), whereas `map` is typically some form of search-tree, which is `O( log n )` for lookups. `unordered_map`, in theory (and in practice) should always be faster than `map` for lookups - so if `map` is faster then something inefficient is going on w.r.t. how your keys are being looked-up. – Dai Nov 27 '20 at 00:13
  • If you worry of that concatenation just use `std::pair` as a key, and don't do the concatenation. – Slava Nov 27 '20 at 00:14
  • 2
    @Slava Did you read the full question? – ypnos Nov 27 '20 at 00:15
  • @ypnos Seeing OP's claim that unordered_map is only slightly faster I suspect that either it was not done properly (testing with `std::pair` or `std::unordered_map` or both( or we are missing some information here. – Slava Nov 27 '20 at 00:32
  • When you concatenate the strings to make the key, where are the two sub strings coming from? If they are dynamically allocated then they may be from random places in memory and killing the cache. But if they can come from a lookup table that is contiguous then copying the strings for std::pair or string concatenation will be slower than using references to the table which wouldn't kill the cache and wouldn't have to copy anything. – Jerry Jeremiah Nov 27 '20 at 01:05
  • Can't you just keep both keys in a string and maybe use string view to only look at the first part? Is one of the keys the same across strings? Why do you need 2 keys ... isn't it possible to use 1? – JMRC Nov 27 '20 at 01:15
  • @Dai In all tests I have ever performed, I found that in practice `std::map` was faster than `std::unorder_map`. Do you have some references showing that *in practice` the hash map is faster? I have to admit that i did not perform tests with an infinite size, roughly up to about 10^7. Anticipating your feedback if any, yes I have some experience (40 years) in making such benchmarks. Of course I agree that the result may depend on the exact scenario. But it is not fair saying that OP is obviously wrong – Damien Nov 27 '20 at 08:35
  • 1
    @jpmorr One last thought on this you might look into. When you have an ordered map of strings, the string comparison op is used, which does lexicographic sorting. So for traversing the map to find the right key, it will have to go over the string until characters do not match. As you don't care about the ordering, you could use your own comparator that first checks the string lengths, then their contents. It might help. The reason unordered map is not faster for you might be related: It always needs to go over the whole string to compute its hash value. – ypnos Nov 27 '20 at 11:49
  • @ypnos Thanks for the idea. I've done a quick search and possibly found something like what you mean: https://stackoverflow.com/a/5733353/1506763 . I can give this a try and it may have an impact if the two group names used to make up the string are significantly different in length. – jpmorr Nov 27 '20 at 12:13
  • If you want speed don't use any map-ish type in std::, find one on github. They're all better. std map-ish types are all very bad performance which is required by their operation and runtime requirements. They're just defined in about the worst way possible to allow for good performance. – xaxxon Nov 29 '20 at 06:29
  • @Damien `std::unordered_map` is almost always faster for large maps. But it can be slower in many cases, for example if the keys are long strings, because calculating the hash now takes even more time than comparing 2 strings which may break just at the beginnings of the strings. See some benchmarks here [Is an unordered_map really faster](https://stackoverflow.com/q/36392583/995714), [Is there any advantage of using map](https://stackoverflow.com/q/2196995/995714). A fast hash function is crucial to a fast hash map. For example there are SIMD hash solutions which are lightning fast – phuclv Nov 29 '20 at 08:47
  • @xaxxon it's not that std:: functions are slow because of the requirements. It's because they're designed to work for all kinds of data, for example they use a common memory allocator that's good for all general allocations, either small or large, but that also means they're shitty for specific cases. That's why C++ allows us to customize lots of things in standard containers like custom allocators or custom hashing functions. There are lots of faster hash maps solutions out there like [Google's sparsehash](https://github.com/sparsehash/sparsehash) – phuclv Nov 29 '20 at 08:52
  • check out also [Super high performance C/C++ hash map](https://stackoverflow.com/q/3300525/995714), [dense_hash_map](http://goog-sparsehash.sourceforge.net/doc/dense_hash_map.html), [boost::flat_map](https://stackoverflow.com/q/21166675/995714), https://github.com/greg7mdp/sparsepp, [C++Now 2018: You Can Do Better than std::unordered_map: New Improvements to Hash Table Performance](https://youtu.be/M2fKMP47slQ), [CppCon 2017: Matt Kulukundis “Designing a Fast, Efficient, Cache-friendly Hash Table, Step by Step”](https://youtu.be/ncHmEUmJZf4) – phuclv Nov 29 '20 at 08:53
  • [Benchmark of major hash maps implementations](https://tessil.github.io/2016/08/29/benchmark-hopscotch-map.html) – phuclv Nov 29 '20 at 08:56
  • @phuclv Thank you for the links. It is what I was looking for. In the test I performed, the building was predominant. That said, in a specific critical situation, nothing can replace a specific benchmark. – Damien Nov 29 '20 at 09:12
  • @phuclv no it's not that at all. It's that they're essentially required to use linked lists, which have terrible memory locality. – xaxxon Nov 29 '20 at 17:41
  • @xaxxon collisions are usually rare. And even without collision it's still much slower than other solutions. Good hashing solutions may reach 20-30GB/s or so. No so with std::hash. The OP has only tens of cases so a collision is unlikely – phuclv Nov 30 '20 at 00:33

1 Answers1

0

You have only a limited number of keys which makes calculating the hash expensive compared to the real lookup. That's why std::map and std::unordered_map aren't much different in your case. Besides JOIN_STRING also introduces unnecessary operations while computing hash or comparing strings

I suggest you to avoid those group names altogether and use group IDs instead. With N group types you only have N2 different types of interactions. Then the IDs will belong to a range of [0, N). If N is known at compile time you can even make it an array. So instead of

string nKey = key1;
nKey += JOIN_STRING;
nKey += key2;

you'll use

std::vector<testvals> vals(N*N);    // vector with N² elements

uint32_t nKey = key1*N + key2;      // index of the <key1, key2> mapping
const auto &val = vals[nKey];       // get the mapped value

You should use & to get a reference instead of a copy. You can also use a map instead of a vector. It's still much slower than a vector but still much faster than a map of string. You can calculate the mapped key like above, or use some mappings like nKey = (key1 << 16) ^ key2 or nKey = ((uint64_t)key1 << 32) | key2

Group names are only used when you convert the names to ID at the beginning, or when you want to print them out. You can use some struct like this to store the name

struct GroupInfo
{
    std::string groupName;
    uint32_t groupID;
}

No need to use typedef for structs in C++ like in your code. You can also use std::vector<std::string> or std::map<uint32_t, std::string> to map from ID to name. The ID can be a smaller type like uint8_t or uint16_t

phuclv
  • 37,963
  • 15
  • 156
  • 475
  • OK this sounds like a good approach - using an int as an index. I'm a bit of a beginner so the only thing the seems a little confusing at the moment is the assignment of the ID to the group name in the `GroupInfo`, as I assume that there's no automatic way to add a new `groupName` while checking for the same name and generating an new ID automatically. I will also have to look up the `GroupInfo` struct every time to get the ID because all I have access to in the code is the string 'groupName` - so won't searching the struct also be slow? – jpmorr Nov 29 '20 at 12:09
  • @jpmorr you never needs to get IDs from name. In your code you'll only ever use the name on creating the map or print the name for the user to see. In all other places only the IDs are used. Make `key1` and `key2` integers instead of storing them as string and then get the ID for looking up – phuclv Nov 29 '20 at 15:27
  • I get that only the IDs should be used, but the variables that I have access to each time are the names. I'm not able to go back and change the main code to return an ID rather than a name as that code is well established and off limits to me. I get returned two string to go lookup whatever information I need. Hence creating a map with a combined string. Before each lookup on the vector I still need to convert the names to IDs to generate the `int` index, but this may not be any quicker than the hashing function in the map. – jpmorr Nov 29 '20 at 15:37
  • yes you do need to change the whole program in order to use this. If you're only passed 2 strings in a small function and can't change things outside it then you'll need a different method – phuclv Nov 29 '20 at 15:41
  • but if you're given the 2 strings and then use it many times in the function then it's worth converting them to the ID. And one more mistake many people do all the time is looking up the same key many times like `if (mymap[key] == something) mymap[key] = another_thing`. Just store the reference or iterator to the found item and reuse it – phuclv Nov 29 '20 at 15:56
  • Thanks. At the moment I've stored them in a structure because the contents are used many times and I don't want to keep doing a lookup so I already call the lookup once and store into a variable that I then use many times. But the one lookup is still a significant cost in comparison to the rest of the code which is basically just lots of multiplication and addition. – jpmorr Nov 29 '20 at 16:12
  • @jpmorr try other hashing libraries as I commented above first to see if they fit. Then if not then write a custom mapping solution – phuclv Nov 29 '20 at 16:14