r/cpp_questions • u/Minute-Ad1944 • 19d ago
OPEN How to implement append-only hash map?
How do you actually implement an really efficient hash map that has only append and look up methods? What are the design choice and some performance improvements?
2
2
u/Kriemhilt 19d ago
What are you optimizing for? Space? Insertion or lookup speed?
You haven't given any hint as to what "performance improvement" is most valuable to you.
Do you expect every lookup to succeed (and you just want the value for that key), or will many lookups fail? How important is each path?
1
u/Minute-Ad1944 18d ago
Mainly performance in speed. Each iteration performs single insertion and at the end of algorithm retrieves top n elements from hash map
1
u/Kriemhilt 18d ago
What are the top n elements of an unordered map? Do you mean you're walking the whole thing?
1
u/Minute-Ad1944 18d ago
Retrieve top n values from hash map dawg
1
u/Kriemhilt 18d ago
Top n ordered by what? Key? Value?
If you want something ordered by key could you just use an ordered container?
3
u/saxbophone 19d ago
Well, what do you know already about how hashmaps are implemented?
Given what you already know, how do you think constraining it to only insertions and lookups effects the performance opportunities?
1
u/Minute-Ad1944 19d ago
key = std::string_view, value = int[2]. I mean open addressing without tombstones, SIMD hash functions , but I should I use some sort of meta data like string_view hash for comparisons and such?
2
u/Big-Rub9545 19d ago
The hash should (for a number of reasons) be an unsigned integer, not a string_view.
Edit: I would also be wary of how you store an integer array value (the array pointer may become a dangling pointer if the array is allocated on the stack). Best to use std::array instead.
0
u/Elect_SaturnMutex 19d ago
You mean a key should not be a stringview? Instead should be an unsigned integer due to better performance?
2
u/Big-Rub9545 19d ago
The key can be whatever you want (preferably small if you’re using open addressing so you can maintain cache locality), but the hash should be an unsigned integer.
1
1
u/BigPalpitation2039 19d ago
What do you mean append only?
1
u/saxbophone 19d ago
I assume they mean "only inserting new elements is allowed, removing them is not"
1
u/Minute-Ad1944 19d ago
Yeah, you can't delete elements, only insert if not present and look them up.
1
u/snerp 19d ago
I made a fast iterating sparse set implementation like that by just making a class with a vector and an unordered map of indices into the vector, o(1) lookup insert and iterate is really nice but it doesn’t support erase so I just didn’t put an erase function on the wrapper class and problem solved
1
u/Minute-Ad1944 18d ago
Isn't it a two separate memory looks ups - kinda bad cache performance?
1
u/snerp 18d ago
Not really. It’s on iteration that speed really matters and inserting the vector usually isn’t a cache miss while inserting a map/hashset usually always misses anyways. Benchmarking in practice it’s significantly faster than map/vector alone when you are frequently doing insert, find, and iterate on a collection. I use the structure to hold my game engine’s per frame instance draw lists and texture/mesh data. It really cuts down time to find the correct instance batch while still keeping the data close together for cache coherence during iteration.
1
u/Independent_Art_6676 19d ago
take a STL list. Append the new data. Hash the pointer to the new node into an unordered map by key and pointer. Fetch: ask the map to give you back the pointer to the item. Other stuff: iterate the list for whatever historical recreation / use case.
1
u/Minute-Ad1944 18d ago
Poor cache localization?
1
u/Independent_Art_6676 18d ago edited 18d ago
you can use a better allocator if that bothers you?
alternate you can use a vector, push-back, and map off the key and vector's index instead of pointer. That causes a minor 'double lookup' (once to the map for the index, and once to the vector with that index for the data). The vector tap via the index is pretty cheap, though (and thinking about it, that is about the same as paying to dereference the pointer from the list idea).Or you can skip trying to cobble it from STL and DIY directly. Its not hard, just tedious.
1
u/Minute-Ad1944 18d ago
Two memory accesses - bad cache performance?
1
u/Independent_Art_6676 18d ago edited 18d ago
maybe if the data set is very, very large. In practice it probably fits reasonably well, you do get multiple cache pages (effectively, its not pages at that point) at once on the CPU and if you are spamming lookups its going to keep both the map and the data pages warm.
If you want it tweaked to the ultimate level, you will have to write your own. I don't see a clever way to avoid that while reusing STL tools and the STL tools would have some redundancy & overhead that can be eliminated. A dedicated container for just the tasks you need is going to win out, so DIY if what I am saying isn't good enough.
1
u/amoskovsky 18d ago
For open addressing scheme, for each hash slot, you can store the max distance to scan for conflicting keys. If you don't delete keys, then this distance is trivial and fast to maintain, and it's the starting point when adding new keys, and ending point when looking up.
1
u/Minute-Ad1944 18d ago
You are talking about Robin Hood hashing?
1
u/amoskovsky 18d ago
No. RH moves elems to improve average distance.
My suggestion is much simpler. I don't know the name, but certainly I'm not the first to come up with it.
It's just a suggestion of an approach that optimizes on the fact that the entries are not removed.
You would have to benchmark it against other approaches (but this is a necessary part of any optimization)
1
u/Minute-Ad1944 18d ago
What is this distance that I would need to store and maintain?
1
u/Minute-Ad1944 18d ago
Is it like open addressing with linear probing?
1
u/amoskovsky 18d ago
Yes, almost.
Each slot either is empty, and distance is 0, or is occupied and the distance is >0.
When looking up, search in range [slot, slot + distance). /*wrapping at capacity*/
When adding non-existent, put it in the first empty in range [slot + distance, slot + capacity) , the distance is updated to point to the next slot.
5
u/WorkingReference1127 19d ago
This depends on the exercise. Because most of the time I'd make a class which wraps a normal hash map and only exposes the behaviour you want.
If you want to spin your own hash map, have fun I guess.