Feature/concurrent node containers (#271)

* added concurrent node containers

* removed spurious typename

* added missing includes

* avoided unused param warning

* worked around Clang bug

* s/{}/() to work around GCC4.8 problems with aggregate initialization

* used /bigobj for cfoa/visit_tests.cpp

* suppressed localized maybe-uninitialized warnings

* fixed comments

* added /bigobj to cfoa/insert_tests.cpp

* instrumented double exact comparison to spot a spurious error

* fixed pedantic error

* refactored byte_span machinery

* compromised on sub-epsilon equality for doubles that should be identical

* documented boost::concurrent_node_(map|set)

* added concurrent_node_set

* added missing AlternativeType

* tested empty node insertion

* tested node_handle allocator management

* added nonassignable_allocator and node_handle_allocator_swap_tests

* fixed warning disabling

* silenced spurious GCC warning

* broadened scope of previous pragma

* broadened even more

* worked around spurious constexpr-related msvc-14.0 bug
https://godbolt.org/z/v78545Ebf

* added workaround back

* replaced previous workaround with built-in one

* added workaround back on top of built-in solution (which doesn't work 100% of the time)
This commit is contained in:
joaquintides
2024-08-25 18:34:58 +02:00
committed by GitHub
parent 35bdabf259
commit f734e399e3
58 changed files with 7007 additions and 338 deletions
+1 -1
View File
@@ -126,7 +126,7 @@ h|*Method* h|*Description*
|`float max_load_factor(float z)`
|Changes the container's maximum load factor, using `z` as a hint. +
**Open-addressing containers:** this function does nothing: users are not allowed to change the maximum load factor.
**Open-addressing and concurrent containers:** this function does nothing: users are not allowed to change the maximum load factor.
|`void rehash(size_type n)`
|Changes the number of buckets so that there at least `n` buckets, and so that the load factor is less than the maximum load factor.
+2 -1
View File
@@ -6,8 +6,9 @@
:github-pr-url: https://github.com/boostorg/unordered/pull
:cpp: C++
== Release 1.87.0
== Release 1.87.0 - Major update
* Added concurrent, node-based containers `boost::concurrent_node_map` and `boost::concurrent_node_set`.
* Made visitation exclusive-locked within certain
`boost::concurrent_flat_set` operations to allow for safe mutable modification of elements
({github-pr-url}/265[PR#265^]).
+5 -3
View File
@@ -89,7 +89,8 @@ The main differences with C++ unordered associative containers are:
== Concurrent Containers
There is currently no specification in the C++ standard for this or any other type of concurrent
data structure. The APIs of `boost::concurrent_flat_set` and `boost::concurrent_flat_map`
data structure. The APIs of `boost::concurrent_flat_set`/`boost::concurrent_node_set` and
`boost::concurrent_flat_map`/`boost::concurrent_node_map`
are modelled after `std::unordered_flat_set` and `std::unordered_flat_map`, respectively,
with the crucial difference that iterators are not provided
due to their inherent problems in concurrent scenarios (high contention, prone to deadlocking):
@@ -105,7 +106,7 @@ In a non-concurrent unordered container, iterators serve two main purposes:
* Access to an element previously located via lookup.
* Container traversal.
In place of iterators, `boost::concurrent_flat_set` and `boost::concurrent_flat_map` use _internal visitation_
In place of iterators, Boost.Unordered concurrent containers use _internal visitation_
facilities as a thread-safe substitute. Classical operations returning an iterator to an
element already existing in the container, like for instance:
@@ -141,7 +142,8 @@ respectively, here visitation is granted mutable or const access depending on
the constness of the member function used (there are also `*cvisit` overloads for
explicit const visitation); In the case of `boost::concurrent_flat_set`, visitation is always const.
One notable operation not provided by `boost::concurrent_flat_map` is `operator[]`/`at`, which can be
One notable operation not provided by `boost::concurrent_flat_map`/`boost::concurrent_node_map`
is `operator[]`/`at`, which can be
replaced, if in a more convoluted manner, by
xref:#concurrent_flat_map_try_emplace_or_cvisit[`try_emplace_or_visit`].
+29 -10
View File
@@ -3,7 +3,8 @@
:idprefix: concurrent_
Boost.Unordered provides `boost::concurrent_flat_set` and `boost::concurrent_flat_map`,
Boost.Unordered provides `boost::concurrent_node_set`, `boost::concurrent_node_map`,
`boost::concurrent_flat_set` and `boost::concurrent_flat_map`,
hash tables that allow concurrent write/read access from
different threads without having to implement any synchronzation mechanism on the user's side.
@@ -43,7 +44,7 @@ logical cores in the CPU).
== Visitation-based API
The first thing a new user of `boost::concurrent_flat_set` or `boost::concurrent_flat_map`
The first thing a new user of Boost.Unordered concurrent containers
will notice is that these classes _do not provide iterators_ (which makes them technically
not https://en.cppreference.com/w/cpp/named_req/Container[Containers^]
in the C++ standard sense). The reason for this is that iterators are inherently
@@ -62,7 +63,7 @@ thread issues an `m.erase(k)` operation between A and B. There are designs that
can remedy this by making iterators lock the element they point to, but this
approach lends itself to high contention and can easily produce deadlocks in a program.
`operator[]` has similar concurrency issues, and is not provided by
`boost::concurrent_flat_map` either. Instead, element access is done through
`boost::concurrent_flat_map`/`boost::concurrent_node_map` either. Instead, element access is done through
so-called _visitation functions_:
[source,c++]
@@ -112,7 +113,7 @@ if (found) {
}
----
Visitation is prominent in the API provided by `boost::concurrent_flat_set` and `boost::concurrent_flat_map`, and
Visitation is prominent in the API provided by concurrent containers, and
many classical operations have visitation-enabled variations:
[source,c++]
@@ -125,13 +126,15 @@ m.insert_or_visit(x, [](auto& y) {
----
Note that in this last example the visitation function could actually _modify_
the element: as a general rule, operations on a `boost::concurrent_flat_map` `m`
the element: as a general rule, operations on a concurrent map `m`
will grant visitation functions const/non-const access to the element depending on whether
`m` is const/non-const. Const access can be always be explicitly requested
by using `cvisit` overloads (for instance, `insert_or_cvisit`) and may result
in higher parallelization. For `boost::concurrent_flat_set`, on the other hand,
in higher parallelization. For concurrent sets, on the other hand,
visitation is always const access.
Consult the references of
xref:#concurrent_node_set[`boost::concurrent_node_set`],
xref:#concurrent_flat_map[`boost::concurrent_node_map`],
xref:#concurrent_flat_set[`boost::concurrent_flat_set`] and
xref:#concurrent_flat_map[`boost::concurrent_flat_map`]
for the complete list of visitation-enabled operations.
@@ -245,7 +248,7 @@ may yield worse performance.
== Blocking Operations
``boost::concurrent_flat_set``s and ``boost::concurrent_flat_map``s can be copied, assigned, cleared and merged just like any
Concurrent containers can be copied, assigned, cleared and merged just like any other
Boost.Unordered container. Unlike most other operations, these are _blocking_,
that is, all other threads are prevented from accesing the tables involved while a copy, assignment,
clear or merge operation is in progress. Blocking is taken care of automatically by the library
@@ -258,9 +261,25 @@ reserving space in advance of bulk insertions will generally speed up the proces
== Interoperability with non-concurrent containers
As open-addressing and concurrent containers are based on the same internal data structure,
`boost::unordered_flat_set` and `boost::unordered_flat_map` can
be efficiently move-constructed from `boost::concurrent_flat_set` and `boost::concurrent_flat_map`,
respectively, and vice versa.
they can be efficiently move-constructed from their non-concurrent counterpart, and vice versa.
[caption=, title='Table {counter:table-counter}. Concurrent/non-concurrent interoperatibility']
[cols="1,1", frame=all, grid=all]
|===
^|`boost::concurrent_node_set`
^|`boost::unordered_node_set`
^|`boost::concurrent_node_map`
^|`boost::unordered_node_map`
^|`boost::concurrent_flat_set`
^|`boost::unordered_flat_set`
^|`boost::concurrent_flat_map`
^|`boost::unordered_flat_map`
|===
This interoperability comes handy in multistage scenarios where parts of the data processing happen
in parallel whereas other steps are non-concurrent (or non-modifying). In the following example,
we want to construct a histogram from a huge input vector of words:
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+7 -5
View File
@@ -43,7 +43,8 @@ boost::unordered_node_map
boost::unordered_flat_map
^.^h|*Concurrent*
^|
^| `boost::concurrent_node_set` +
`boost::concurrent_node_map`
^| `boost::concurrent_flat_set` +
`boost::concurrent_flat_map`
@@ -59,6 +60,7 @@ There are two variants: **flat** (the fastest) and **node-based**, which
provide pointer stability under rehashing at the expense of being slower.
* Finally, **concurrent containers** are designed and implemented to be used in high-performance
multithreaded scenarios. Their interface is radically different from that of regular C++ containers.
Flat and node-based variants are provided.
All sets and maps in Boost.Unordered are instantiatied similarly as
`std::unordered_set` and `std::unordered_map`, respectively:
@@ -72,8 +74,8 @@ namespace boost {
class Pred = std::equal_to<Key>,
class Alloc = std::allocator<Key> >
class unordered_set;
// same for unordered_multiset, unordered_flat_set, unordered_node_set
// and concurrent_flat_set
// same for unordered_multiset, unordered_flat_set, unordered_node_set,
// concurrent_flat_set and concurrent_node_set
template <
class Key, class Mapped,
@@ -81,8 +83,8 @@ namespace boost {
class Pred = std::equal_to<Key>,
class Alloc = std::allocator<std::pair<Key const, Mapped> > >
class unordered_map;
// same for unordered_multimap, unordered_flat_map, unordered_node_map
// and concurrent_flat_map
// same for unordered_multimap, unordered_flat_map, unordered_node_map,
// concurrent_flat_map and concurrent_node_map
}
----
+3 -2
View File
@@ -121,7 +121,8 @@ for Visual Studio on an x64-mode Intel CPU with SSE2 and for GCC on an IBM s390x
== Concurrent Containers
The same data structure used by Boost.Unordered open-addressing containers has been chosen
also as the foundation of `boost::concurrent_flat_set` and `boost::concurrent_flat_map`:
also as the foundation of `boost::concurrent_flat_set`/`boost::concurrent_node_set` and
`boost::concurrent_flat_map`/`boost::concurrent_node_map`:
* Open-addressing is faster than closed-addressing alternatives, both in non-concurrent and
concurrent scenarios.
@@ -130,7 +131,7 @@ with minimal locking. In particular, the metadata array can be used for implemen
lookup that are lock-free up to the last step of actual element comparison.
* Layout compatibility with Boost.Unordered flat containers allows for
xref:#concurrent_interoperability_with_non_concurrent_containers[fast transfer]
of all elements between `boost::concurrent_flat_map` and `boost::unordered_flat_map`,
of all elements between a concurrent container and its non-concurrent counterpart,
and vice versa.
=== Hash Function and Platform Interoperability
+2
View File
@@ -13,3 +13,5 @@ include::unordered_node_map.adoc[]
include::unordered_node_set.adoc[]
include::concurrent_flat_map.adoc[]
include::concurrent_flat_set.adoc[]
include::concurrent_node_map.adoc[]
include::concurrent_node_set.adoc[]
+2 -1
View File
@@ -129,7 +129,8 @@ xref:#rationale_open_addresing_containers[corresponding section].
== Concurrent Containers
`boost::concurrent_flat_set` and `boost::concurrent_flat_map` use the basic
`boost::concurrent_flat_set`/`boost::concurrent_node_set` and
`boost::concurrent_flat_map`/`boost::concurrent_node_map` use the basic
xref:#structures_open_addressing_containers[open-addressing layout] described above
augmented with synchronization mechanisms.
+22 -3
View File
@@ -78,6 +78,7 @@ namespace boost {
explicit xref:#unordered_node_map_allocator_constructor[unordered_node_map](const Allocator& a);
xref:#unordered_node_map_copy_constructor_with_allocator[unordered_node_map](const unordered_node_map& other, const Allocator& a);
xref:#unordered_node_map_move_constructor_with_allocator[unordered_node_map](unordered_node_map&& other, const Allocator& a);
xref:#unordered_node_map_move_constructor_from_concurrent_node_map[unordered_node_map](concurrent_node_map<Key, T, Hash, Pred, Allocator>&& other);
xref:#unordered_node_map_initializer_list_constructor[unordered_node_map](std::initializer_list<value_type> il,
size_type n = _implementation-defined_
const hasher& hf = hasher(),
@@ -537,6 +538,24 @@ and always calls `other.reset_stats()`.
---
==== Move Constructor from concurrent_node_map
```c++
unordered_node_map(concurrent_node_map<Key, T, Hash, Pred, Allocator>&& other);
```
Move construction from a xref:#concurrent_node_map[`concurrent_node_map`].
The internal bucket array of `other` is transferred directly to the new container.
The hash function, predicate and allocator are moved-constructed from `other`.
If statistics are xref:unordered_node_map_boost_unordered_enable_stats[enabled],
transfers the internal statistical information from `other` and calls `other.reset_stats()`.
[horizontal]
Complexity:;; Constant time.
Concurrency:;; Blocking on `other`.
---
==== Initializer List Constructor
[source,c++,subs="+quotes"]
----
@@ -1219,8 +1238,8 @@ Throws:;; Nothing.
==== Extract by Key
```c++
node_type erase(const key_type& k);
template<class K> node_type erase(K&& k);
node_type extract(const key_type& k);
template<class K> node_type extract(K&& k);
```
Extracts the element with key equivalent to `k`, if it exists.
@@ -1228,7 +1247,7 @@ Extracts the element with key equivalent to `k`, if it exists.
[horizontal]
Returns:;; A `node_type` object holding the extracted element, or empty if no element was extracted.
Throws:;; Only throws an exception if it is thrown by `hasher` or `key_equal`.
Notes:;; The `template<class K>` overload only participates in overload resolution if `Hash::is_transparent` and `Pred::is_transparent` are valid member typedefs and neither `iterator` nor `const_iterator` are implicitly convertible from `K`. The library assumes that `Hash` is callable with both `K` and `Key` and that `Pred` is transparent. This enables heterogeneous lookup which avoids the cost of instantiating an instance of the `Key` type.
Notes:;; The `template<class K>` overload only participates in overload resolution if `Hash::is_transparent` and `Pred::is_transparent` are valid member typedefs. The library assumes that `Hash` is callable with both `K` and `Key` and that `Pred` is transparent. This enables heterogeneous lookup which avoids the cost of instantiating an instance of the `Key` type.
---
+22 -3
View File
@@ -73,6 +73,7 @@ namespace boost {
explicit xref:#unordered_node_set_allocator_constructor[unordered_node_set](const Allocator& a);
xref:#unordered_node_set_copy_constructor_with_allocator[unordered_node_set](const unordered_node_set& other, const Allocator& a);
xref:#unordered_node_set_move_constructor_with_allocator[unordered_node_set](unordered_node_set&& other, const Allocator& a);
xref:#unordered_node_set_move_constructor_from_concurrent_node_set[unordered_node_set](concurrent_node_set<Key, Hash, Pred, Allocator>&& other);
xref:#unordered_node_set_initializer_list_constructor[unordered_node_set](std::initializer_list<value_type> il,
size_type n = _implementation-defined_
const hasher& hf = hasher(),
@@ -489,6 +490,24 @@ and always calls `other.reset_stats()`.
---
==== Move Constructor from concurrent_node_set
```c++
unordered_node_set(concurrent_node_set<Key, Hash, Pred, Allocator>&& other);
```
Move construction from a xref:#concurrent_node_set[`concurrent_node_set`].
The internal bucket array of `other` is transferred directly to the new container.
The hash function, predicate and allocator are moved-constructed from `other`.
If statistics are xref:unordered_node_set_boost_unordered_enable_stats[enabled],
transfers the internal statistical information from `other` and calls `other.reset_stats()`.
[horizontal]
Complexity:;; Constant time.
Concurrency:;; Blocking on `other`.
---
==== Initializer List Constructor
[source,c++,subs="+quotes"]
----
@@ -1028,8 +1047,8 @@ Throws:;; Nothing.
==== Extract by Key
```c++
node_type erase(const key_type& k);
template<class K> node_type erase(K&& k);
node_type extract(const key_type& k);
template<class K> node_type extract(K&& k);
```
Extracts the element with key equivalent to `k`, if it exists.
@@ -1037,7 +1056,7 @@ Extracts the element with key equivalent to `k`, if it exists.
[horizontal]
Returns:;; A `node_type` object holding the extracted element, or empty if no element was extracted.
Throws:;; Only throws an exception if it is thrown by `hasher` or `key_equal`.
Notes:;; The `template<class K>` overload only participates in overload resolution if `Hash::is_transparent` and `Pred::is_transparent` are valid member typedefs and neither `iterator` nor `const_iterator` are implicitly convertible from `K`. The library assumes that `Hash` is callable with both `K` and `Key` and that `Pred` is transparent. This enables heterogeneous lookup which avoids the cost of instantiating an instance of the `Key` type.
Notes:;; The `template<class K>` overload only participates in overload resolution if `Hash::is_transparent` and `Pred::is_transparent` are valid member typedefs. The library assumes that `Hash` is callable with both `K` and `Key` and that `Pred` is transparent. This enables heterogeneous lookup which avoids the cost of instantiating an instance of the `Key` type.
---