-
-
Notifications
You must be signed in to change notification settings - Fork 77
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adds ankerl::unordered_dense{segmented_map, segmented_set} #58
Conversation
1c939f5
to
fb2c3ec
Compare
fa2c0d7
to
b844618
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For your consideration: Supporting Fancy Pointers
I've had a look at the implementation with regards to support allocators that use fancy pointers. This is especially relevant for boost::interprocess::offset_ptr
which are used to work in shared memory and especially memory mapped files.
I think the changes required to support allocators that use fancy pointers are minimal. I changed the relevant typedefs and function calls where I found them. The changes are untested and probably incomplete.
A simple way to test that it works with offset pointers is either a thin wrapper around std::allocator
that uses boost's offset pointers or to use metall.
include/ankerl/unordered_dense.h
Outdated
using difference_type = std::ptrdiff_t; | ||
using reference = T&; | ||
using const_reference = T const&; | ||
using pointer = T*; | ||
using const_pointer = T const*; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
using difference_type = std::ptrdiff_t; | |
using reference = T&; | |
using const_reference = T const&; | |
using pointer = T*; | |
using const_pointer = T const*; | |
using difference_type = std::allocator_traits<allocator_type>::difference_type; | |
using reference = T&; | |
using const_reference = T const&; | |
using pointer = std::allocator_traits<allocator_type>::pointer; | |
using const_pointer = std::allocator_traits<allocator_type>::const_pointer; |
increase_capacity(); | ||
} | ||
auto* ptr = static_cast<void*>(&operator[](m_size)); | ||
auto& ref = *new (ptr) T(std::forward<Args>(args)...); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
auto& ref = *new (ptr) T(std::forward<Args>(args)...); | |
auto& ref = std::allocator_traits<allocator_type>::construct(get_allocator(), std::forward<Args>(args)...); |
include/ankerl/unordered_dense.h
Outdated
static constexpr auto num_elements_in_block = 1U << num_bits; | ||
static constexpr auto mask = num_elements_in_block - 1U; | ||
|
||
std::vector<T*, vec_alloc> m_blocks{}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
std::vector<T*, vec_alloc> m_blocks{}; | |
vec_type<std::allocator_traits<T>::pointer, vec_alloc> m_blocks{}; |
std::vector
does not work with allocators that require fancy pointers.
Suggestion: make the type of m_blocks configurable via a template parameter
include/ankerl/unordered_dense.h
Outdated
*/ | ||
template <bool IsConst> | ||
class iter_t { | ||
using ptr_t = typename std::conditional_t<IsConst, T const* const*, T**>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
using ptr_t = typename std::conditional_t<IsConst, T const* const*, T**>; | |
using ptr_t = typename std::conditional_t<IsConst, std::pointer_traits<std::allocator_traits<T>::pointer>::template rebind<std::allocator_traits<T>::const_pointer> const, std::pointer_traits<std::allocator_traits<T>::pointer>::template rebind<std::allocator_traits<T>::pointer>>; |
include/ankerl/unordered_dense.h
Outdated
using reference = typename std::conditional<IsConst, value_type const&, value_type&>::type; | ||
using pointer = typename std::conditional<IsConst, value_type const*, value_type*>::type; | ||
using iterator_category = std::forward_iterator_tag; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See line 587
Hi @bigerl! Is the segmented vector of interest to you? It has a much more smooth allocation pattern than std:: vector. I'll add more tests to make sure it works with fancy pointers 👍 |
Hi @martinus. The map with segmented vector would be very interesting. A reference-stable hash map without the overhead of a node-based design sounds very promising. It makes it easy to store things there and still keep stable (offset-)pointers on them. That's something I was looking for in some of my projects for quite some time. In combination with good iteration speed (I would expect it to be similar to |
Note that this implementation still won't have stable references, at least not on |
Ah, right. Just had a look into the code again. Tombstones would probably be the only option to maintain reference validity on erase. It comes with serious issues though: E.g., inserting entries 1...n and erasing entries 1...(n-1) would leaf the map with (n-1) tombstones. Having said that, also without stable references it would use it in shared memory. Especially the fact that the memory footprint stays always close to linear to the number of entries is very nice. |
I could do stable references without tombstones, with an in-place freelist in the |
a9c4a71
to
f336a9b
Compare
This new underlying container has a much smoother memory allocation curve than the default underlying `std::vector`. * Much smoother memory usage, memory usage increases continuously. * No high peak memory usage. * Faster insertion because elements never need to be moved to new allocated blocks * Slightly slower indexing compared to `std::vector` because an additional indirection is needed. Abseil is fastest for this simple inserting test, taking a bit over 0.8 seconds. It's peak memory usage is about 430 MB. Note how the memory usage goes down after the last peak; when it goes down to ~290MB it has finished rehashing and could free the previously used memory block. `ankerl::unordered_dense::segmented_map` doesn't have these peaks, and instead has a smooth increase of memory usage. Note there are still sudden drops & increases in memory because the indexing data structure needs still needs to increase by a fixed factor. But due to holding the data in a separate container we are able to first free the old data structure, and then allocate a new, bigger indexing structure; thus we do not have peaks. bump to 4.0.0
f336a9b
to
3b51559
Compare
This new underlying container has a much smoother memory allocation curve
than the default underlying
std::vector
.std::vector
because an additionalindirection is needed.
Abseil is fastest for this simple inserting test, taking a bit over 0.8 seconds.
It's peak memory usage is about 430 MB. Note how the memory usage goes down after
the last peak; when it goes down to ~290MB it has finished rehashing and could free
the previously used memory block.
ankerl::unordered_dense::segmented_map
doesn't have these peaks, and instead hasa smooth increase of memory usage. Note there are still sudden drops & increases in
memory because the indexing data structure needs still needs to increase by a fixed
factor. But due to holding the data in a separate container we are able to first free
the old data structure, and then allocate a new, bigger indexing structure; thus we
do not have peaks.
bump to 4.0.0