mirror of
https://github.com/carbon-language/carbon-lang.git
synced 2026-09-24 11:40:14 +01:00
Replaces the callback-based `ForEach` methods on `RawHashtable`, `Map`,
and
`Set` with a range object supporting range-for loops, structured
bindings, and
the standard range concepts.
- Adds `.entries()` on `Map`, `Set`, and `RawHashtable`, returning a
range that
models `std::ranges::forward_range` and `std::ranges::common_range`.
Obtaining one is an explicit call rather than `begin()`/`end()` on the
container, as scanning a whole table is costly and shouldn't be hidden.
- Iterating a `Map` yields a `std::pair` of key and value references,
which
fits in two registers and is returned without being materialized in
memory.
- `Map::Range` and `Set::Range` are aliases of the raw hashtable's range
rather
than wrappers around it. The raw iterator produces the user-facing
reference
itself -- a `KeyT&` for a set, a pair of references for a map -- picked
by
`StorageEntry`, which is already specialized on whether there is a value
type. That leaves one iterator to reason about instead of three.
- Deletes the rvalue `.entries()` overloads on the owning containers, as
a
range built from a temporary table would dangle. Views don't own their
storage, so the operation remains available on them.
- In release builds, the walk over the groups is a single induction
variable: a
negative byte offset counting up to zero, anchored at the ends of the
metadata and entry arrays. Both arrays are then reached by indexed
addressing
off a base that stays put, and the entry pointer is formed only once a
group
with a present entry has been found.
- In debug builds, the range hashes the table's metadata when it is
built and
re-checks that hash when it is destroyed, catching mutation of the table
while a range is live. It also picks a random starting group and a
random odd
group stride, which varies the traversal order between ranges while
still
visiting every group exactly once. That entropy is drawn when the range
is
built rather than in `begin()`, so `begin()` stays a pure function of
the
range and the multi-pass guarantee holds.
- Removes `ForEachEntry` and all of its callers.
Measured against the iteration benchmark added in its own commit, a
traversal is at or ahead of what the callback compiled to across nearly
the
whole size range. The largest tables spend 3-5% fewer cycles, small
`Set`s as
much as 24% fewer, and instruction counts stay within about 1%. What
remains
behind is a handful of mid-sized `Map`s by up to 1%, and `Set` at 65536,
which
sits at exactly half its load factor, by 2%.
Both revisions were built with `-c opt --copt=-march=x86-64-v3` and
compared
with:
```
./scripts/bench_runner.py --exp_benchmark=... --base_benchmark=... \
--benchmark_args=--benchmark_perf_counters=INSTRUCTIONS,CYCLES \
--benchmark_args='--benchmark_filter=(Set|Map)Iterate<(Set|Map)<' \
--extra_metrics_filter='(INSTRUCTIONS|CYCLES)'
```
Trimmed below to the primary integer configurations and to the two
counters;
the pointer- and string-keyed configurations follow the same pattern.
```
Benchmark ┃ CYCLES ┃ INSTRUCTIONS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
BM_MapIterate<Map<int, int>>/1....... │ 👍 -6.032% p=1.14e-05 │ ?? p=0.752
baseline: │ 12.06 ± 1.520% │ 64 ± 3.125%
experiment: │ 11.33 ± 2.765% │ 65.5 ± 3.817%
│ │
BM_MapIterate<Map<int, int>>/2....... │ ?? p=0.155 │ ?? p=0.343
baseline: │ 7.587 ± 1.285% │ 41 ± 0.000%
experiment: │ 7.652 ± 0.865% │ 41 ± 2.439%
│ │
BM_MapIterate<Map<int, int>>/3....... │ ?? p=0.343 │ 👍 -1.020% p=0.0039
baseline: │ 6.663 ± 4.260% │ 32.67 ± 2.041%
experiment: │ 6.368 ± 12.224% │ 32.33 ± 2.062%
│ │
BM_MapIterate<Map<int, int>>/4....... │ ?? p=0.343 │ 👍 -1.786% p=0.0297
baseline: │ 6.091 ± 15.470% │ 28 ± 3.571%
experiment: │ 5.957 ± 8.932% │ 27.5 ± 3.636%
│ │
BM_MapIterate<Map<int, int>>/8....... │ ?? p=0.323 │ 👍 -1.220% p=0.000148
baseline: │ 4.845 ± 0.800% │ 20.5 ± 0.000%
experiment: │ 4.814 ± 3.585% │ 20.25 ± 0.000%
│ │
BM_MapIterate<Map<int, int>>/16...... │ 👍 -2.195% p=0.00908 │ 👍 0.769% p=6.58e-06
baseline: │ 4.312 ± 0.187% │ 16.25 ± 0.000%
experiment: │ 4.218 ± 2.368% │ 16.13 ± 0.000%
│ │
BM_MapIterate<Map<int, int>>/32...... │ ?? p=0.236 │ 👍 0.442% p=9.53e-06
baseline: │ 4.051 ± 1.084% │ 14.13 ± 0.000%
experiment: │ 4.063 ± 0.737% │ 14.06 ± 0.000%
│ │
BM_MapIterate<Map<int, int>>/64...... │ ?? p=0.693 │ 👎 0.227% p=4.52e-06
baseline: │ 4.021 ± 0.239% │ 13.75 ± 0.000%
experiment: │ 4.019 ± 0.417% │ 13.78 ± 0.000%
│ │
BM_MapIterate<Map<int, int>>/256..... │ 👍 0.360% p=0.00119 │ 👎 0.754% p=1.37e-05
baseline: │ 3.996 ± 0.173% │ 13.47 ± 0.000%
experiment: │ 3.982 ± 0.272% │ 13.57 ± 0.000%
│ │
BM_MapIterate<Map<int, int>>/4096.... │ 👍 0.581% p=1.96e-05 │ 👎 0.923% p=1.96e-05
baseline: │ 4.005 ± 0.816% │ 13.38 ± 0.000%
experiment: │ 3.981 ± 0.192% │ 13.5 ± 0.000%
│ │
BM_MapIterate<Map<int, int>>/65536... │ 👍 -4.957% p=1.14e-05 │ 👎 0.934% p=1.14e-05
baseline: │ 5.307 ± 0.501% │ 13.38 ± 0.000%
experiment: │ 5.044 ± 1.746% │ 13.5 ± 0.000%
│ │
BM_MapIterate<Map<int, int>>/1048576. │ 👍 -3.947% p=9.09e-05 │ 👎 0.935% p=3.3e-05
baseline: │ 6.074 ± 0.807% │ 13.38 ± 0.000%
experiment: │ 5.834 ± 2.159% │ 13.5 ± 0.000%
│ │
BM_MapIterate<Map<int, int>>/16777216 │ ?? p=0.155 │ 👎 0.935% p=2.11e-05
baseline: │ 5.082 ± 3.650% │ 13.38 ± 0.000%
experiment: │ 5.012 ± 1.316% │ 13.5 ± 0.000%
│ │
BM_MapIterate<Map<int, int>>/56...... │ 👎 0.825% p=0.0268 │ 👍 0.270% p=1.14e-05
baseline: │ 3.918 ± 0.501% │ 13.21 ± 0.000%
experiment: │ 3.951 ± 0.342% │ 13.18 ± 0.000%
│ │
BM_MapIterate<Map<int, int>>/224..... │ 👎 0.788% p=0.000504 │ 👎 0.346% p=1.64e-05
baseline: │ 3.895 ± 0.111% │ 12.89 ± 0.000%
experiment: │ 3.926 ± 0.285% │ 12.94 ± 0.000%
│ │
BM_MapIterate<Map<int, int>>/3584.... │ 👎 1.028% p=0.000148 │ 👎 0.545% p=1.14e-05
baseline: │ 3.913 ± 0.427% │ 12.79 ± 0.000%
experiment: │ 3.954 ± 0.325% │ 12.86 ± 0.000%
│ │
BM_MapIterate<Map<int, int>>/57344... │ ?? p=0.236 │ 👎 0.558% p=2.55e-06
baseline: │ 4.574 ± 0.721% │ 12.79 ± 0.000%
experiment: │ 4.51 ± 3.709% │ 12.86 ± 0.000%
│ │
BM_MapIterate<Map<int, int>>/917504.. │ 👍 -3.826% p=6.58e-06 │ 👎 0.559% p=2.33e-05
baseline: │ 5.221 ± 0.507% │ 12.79 ± 0.000%
experiment: │ 5.021 ± 0.556% │ 12.86 ± 0.000%
│ │
BM_MapIterate<Map<int, int>>/14680064 │ 👍 -3.839% p=1.37e-05 │ 👎 0.559% p=3.31e-05
baseline: │ 5.129 ± 1.194% │ 12.79 ± 0.000%
experiment: │ 4.932 ± 1.475% │ 12.86 ± 0.000%
│ │
Benchmark ┃ CYCLES ┃ INSTRUCTIONS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
BM_SetIterate<Set<int>>/1....... │ 👍 -3.104% p=0.000583 │ ?? p=0.206
baseline: │ 11.2 ± 6.323% │ 60 ± 3.333%
experiment: │ 10.85 ± 5.820% │ 61 ± 3.279%
│ │
BM_SetIterate<Set<int>>/2....... │ 👍 -7.037% p=0.0362 │ ?? p=0.155
baseline: │ 7.086 ± 16.857% │ 37 ± 0.000%
experiment: │ 6.587 ± 0.479% │ 36 ± 4.167%
│ │
BM_SetIterate<Set<int>>/3....... │ 👎 1.400% p=2.34e-05 │ 👍 -1.163% p=0.00136
baseline: │ 5.363 ± 0.463% │ 28.67 ± 2.326%
experiment: │ 5.438 ± 32.763% │ 28.33 ± 1.176%
│ │
BM_SetIterate<Set<int>>/4....... │ ?? p=0.968 │ ?? p=0.286
baseline: │ 4.642 ± 32.751% │ 23.5 ± 2.128%
experiment: │ 4.658 ± 38.416% │ 23.63 ± 3.704%
│ │
BM_SetIterate<Set<int>>/8....... │ 👍 -23.823% p=3.74e-06 │ 👍 -1.515% p=5.52e-05
baseline: │ 4.701 ± 6.589% │ 16.5 ± 0.000%
experiment: │ 3.581 ± 7.790% │ 16.25 ± 0.000%
│ │
BM_SetIterate<Set<int>>/16...... │ 👍 -4.502% p=1.37e-05 │ 👍 -1.020% p=3.31e-05
baseline: │ 3.124 ± 0.585% │ 12.25 ± 0.000%
experiment: │ 2.983 ± 0.625% │ 12.13 ± 0.000%
│ │
BM_SetIterate<Set<int>>/32...... │ 👍 -4.032% p=5.46e-06 │ 👍 0.617% p=1.96e-05
baseline: │ 2.957 ± 0.260% │ 10.13 ± 0.000%
experiment: │ 2.838 ± 0.434% │ 10.06 ± 0.000%
│ │
BM_SetIterate<Set<int>>/64...... │ 👍 -5.054% p=4.52e-06 │ 👎 0.321% p=1.37e-05
baseline: │ 2.937 ± 0.301% │ 9.75 ± 0.000%
experiment: │ 2.788 ± 1.143% │ 9.781 ± 0.000%
│ │
BM_SetIterate<Set<int>>/256..... │ 👍 -5.325% p=1.14e-05 │ 👎 1.073% p=6.58e-06
baseline: │ 2.916 ± 0.220% │ 9.469 ± 0.000%
experiment: │ 2.761 ± 0.142% │ 9.57 ± 0.000%
│ │
BM_SetIterate<Set<int>>/4096.... │ 👍 -4.865% p=4.52e-06 │ 👎 1.317% p=2.34e-05
baseline: │ 2.921 ± 0.194% │ 9.381 ± 0.000%
experiment: │ 2.779 ± 0.224% │ 9.504 ± 0.000%
│ │
BM_SetIterate<Set<int>>/65536... │ 👎 1.961% p=3.93e-05 │ 👎 1.332% p=1.49e-05
baseline: │ 4.015 ± 0.482% │ 9.375 ± 0.000%
experiment: │ 4.094 ± 0.613% │ 9.5 ± 0.000%
│ │
BM_SetIterate<Set<int>>/1048576. │ 👍 -4.843% p=1.14e-05 │ 👎 1.333% p=5.38e-06
baseline: │ 5.239 ± 0.144% │ 9.375 ± 0.000%
experiment: │ 4.986 ± 0.139% │ 9.5 ± 0.000%
│ │
BM_SetIterate<Set<int>>/16777216 │ 👍 0.840% p=0.0362 │ 👎 1.333% p=2.52e-06
baseline: │ 3.719 ± 1.420% │ 9.375 ± 0.000%
experiment: │ 3.688 ± 1.308% │ 9.5 ± 0.000%
│ │
BM_SetIterate<Set<int>>/56...... │ 👍 -2.857% p=9.53e-06 │ 👍 0.388% p=3.31e-05
baseline: │ 2.942 ± 0.439% │ 9.214 ± 0.000%
experiment: │ 2.858 ± 0.619% │ 9.179 ± 0.000%
│ │
BM_SetIterate<Set<int>>/224..... │ 👍 -2.161% p=2.34e-05 │ 👎 0.502% p=4.52e-06
baseline: │ 2.888 ± 0.347% │ 8.893 ± 0.000%
experiment: │ 2.826 ± 0.450% │ 8.938 ± 0.000%
│ │
BM_SetIterate<Set<int>>/3584.... │ 👍 -1.750% p=6.58e-06 │ 👎 0.793% p=2.34e-05
baseline: │ 2.89 ± 0.261% │ 8.792 ± 0.000%
experiment: │ 2.84 ± 0.411% │ 8.862 ± 0.000%
│ │
BM_SetIterate<Set<int>>/57344... │ ?? p=0.502 │ 👎 0.812% p=2.78e-05
baseline: │ 3.684 ± 4.246% │ 8.786 ± 0.000%
experiment: │ 3.644 ± 4.431% │ 8.857 ± 0.000%
│ │
BM_SetIterate<Set<int>>/917504.. │ 👍 -2.629% p=0.000148 │ 👎 0.813% p=3.08e-06
baseline: │ 4.372 ± 0.693% │ 8.786 ± 0.000%
experiment: │ 4.257 ± 0.210% │ 8.857 ± 0.000%
│ │
BM_SetIterate<Set<int>>/14680064 │ 👍 -2.927% p=0.0219 │ 👎 0.813% p=3.03e-06
baseline: │ 4.154 ± 3.286% │ 8.786 ± 0.000%
experiment: │ 4.032 ± 3.198% │ 8.857 ± 0.000%
│ │
```
Assisted-by: Antigravity with Opus
2074 lines
84 KiB
C++
2074 lines
84 KiB
C++
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
|
|
// Exceptions. See /LICENSE for license information.
|
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
|
|
#ifndef CARBON_COMMON_RAW_HASHTABLE_H_
|
|
#define CARBON_COMMON_RAW_HASHTABLE_H_
|
|
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <concepts>
|
|
#include <cstddef>
|
|
#include <cstring>
|
|
#include <iterator>
|
|
#include <new>
|
|
#include <type_traits>
|
|
#include <utility>
|
|
|
|
#include "common/check.h"
|
|
#include "common/concepts.h"
|
|
#include "common/hashing.h"
|
|
#include "common/raw_hashtable_metadata_group.h"
|
|
#include "llvm/ADT/iterator.h"
|
|
#include "llvm/Support/Compiler.h"
|
|
#include "llvm/Support/MathExtras.h"
|
|
|
|
// A namespace collecting a set of low-level utilities for building hashtable
|
|
// data structures. These should only be used as implementation details of
|
|
// higher-level data-structure APIs.
|
|
//
|
|
// The utilities here use the `hashtable_key_context.h` provided `KeyContext` to
|
|
// support the necessary hashtable operations on keys: hashing and comparison.
|
|
// This also serves as the customization point for hashtables built on this
|
|
// infrastructure for those operations. See that header file for details.
|
|
//
|
|
// These utilities support hashtables following a *specific* API design pattern,
|
|
// and using Small-Size Optimization, or "SSO", when desired. We expect there to
|
|
// be three layers to any hashtable design:
|
|
//
|
|
// - A *view* type: a read-only view of the hashtable contents. This type should
|
|
// be a value type and is expected to be passed by-value in APIs. However, it
|
|
// will have `const`-reference semantics, much like a `std::string_view`. Note
|
|
// that the *entries* will continue to be mutable, it is only the *table* that
|
|
// is read-only.
|
|
//
|
|
// - A *base* type: a base class type of the actual hashtable, which allows
|
|
// almost all mutable operations but erases any specific SSO buffer size.
|
|
// Because this is a base of the actual hash table, it is designed to be
|
|
// passed as a non-`const` reference or pointer.
|
|
//
|
|
// - A *table* type: the actual hashtable which derives from the base type and
|
|
// adds any desired SSO storage buffer. Beyond the physical storage, it also
|
|
// allows resetting the table to its initial state & allocated size, as well
|
|
// as copying and moving the table.
|
|
//
|
|
// For complete examples of the API design, see `set.h` for a hashtable-based
|
|
// set data structure, and `map.h` for a hashtable-based map data structure.
|
|
//
|
|
// The hashtable design implemented here has several key invariants and design
|
|
// elements that are essential to all three of the types above and the
|
|
// functionality they provide.
|
|
//
|
|
// - The underlying hashtable uses [open addressing], a power-of-two table size,
|
|
// and quadratic probing rather than closed addressing and chaining.
|
|
//
|
|
// [open addressing]: https://en.wikipedia.org/wiki/Open_addressing
|
|
//
|
|
// - Each _slot_ in the table corresponds to a key, a value, and one byte of
|
|
// metadata. Each _entry_ is a key and value. The key and value for an entry
|
|
// are stored together.
|
|
//
|
|
// - The allocated storage is organized into an array of metadata bytes followed
|
|
// by an array of entry storage.
|
|
//
|
|
// - The metadata byte corresponding to each entry marks that entry is either
|
|
// empty, deleted, or present. When present, a 7-bit tag is also stored using
|
|
// another 7 bits from the hash of the entry key.
|
|
//
|
|
// - The storage for an entry is an internal type that should not be exposed to
|
|
// users, and instead only the underlying keys and values.
|
|
//
|
|
// - The hash addressing and probing occurs over *groups* of slots rather than
|
|
// individual entries. When inserting a new entry, it can be added to the
|
|
// group it hashes to as long it is not full, and can even replace a slot with
|
|
// a tombstone indicating a previously deleted entry. Only when the group is
|
|
// full will it look at the next group in the probe sequence. As a result,
|
|
// there may be entries in a group where a different group is the start of
|
|
// that entry's probe sequence. Also, when performing a lookup, every group in
|
|
// the probe sequence must be inspected for the lookup key until it is found
|
|
// or the group has an empty slot.
|
|
//
|
|
// - Groups are scanned rapidly using the one-byte metadata for each entry in
|
|
// the group and CPU instructions that allow comparing all of the metadata for
|
|
// a group in parallel. For more details on the metadata group encoding and
|
|
// scanning, see `raw_hashtable_metadata_group.h`.
|
|
//
|
|
// - `GroupSize` is a platform-specific relatively small power of two that fits
|
|
// in some hardware register. However, `MaxGroupSize` is provided as a
|
|
// portable max that is also a power of two. The table storage, whether
|
|
// provided by an SSO buffer or allocated, is required to be a multiple of
|
|
// `MaxGroupSize` to keep the requirement portable but sufficient for all
|
|
// platforms.
|
|
//
|
|
// - There is *always* an allocated table of some multiple of `MaxGroupSize`.
|
|
// This allows accesses to be branchless. When heap allocated, we pro-actively
|
|
// allocate at least a minimum heap size table. When there is a small-size
|
|
// optimization (SSO) buffer, that provides the initial allocation.
|
|
//
|
|
// - The table performs a minimal amount of bookkeeping that limits the APIs it
|
|
// can support:
|
|
// - `alloc_size` is the size of the table *allocated* (not *used*), and is
|
|
// always a power of 2 at least as big as `MinAllocatedSize`.
|
|
// - `storage` is a pointer to the storage for the `alloc_size` slots of the
|
|
// table, and never null.
|
|
// - `small_alloc_size` is the maximum `alloc_size` where the table is stored
|
|
// in the object itself instead of separately on the heap. In this case,
|
|
// `storage` points to `small_storage_`.
|
|
// - `growth_budget` is the number of entries that may be added before the
|
|
// table allocation is doubled. It is always
|
|
// `GrowthThresholdForAllocSize(alloc_size)` minus the number of
|
|
// non-empty (filled or deleted) slots. If it ever falls to 0, the table
|
|
// is grown to keep it greater than 0.
|
|
// There is also the "moved-from" state where the table may only be
|
|
// reinitialized or destroyed where the `alloc_size` is 0 and `storage` is
|
|
// null. Since it doesn't track the exact number of filled entries in a table,
|
|
// it doesn't support a container-style `size` API.
|
|
//
|
|
// - Iteration is provided by a range object rather than by iterators hanging
|
|
// directly off the table, because the debug-only checks for mutation during
|
|
// iteration need state that outlives a single iterator: see `EntryRange`
|
|
// below. Obtaining one is an explicit call (`entries()`), as scanning an
|
|
// entire table is a costly operation that shouldn't be hidden behind a bare
|
|
// `begin()`/`end()` pair.
|
|
//
|
|
// The order of iteration is not guaranteed, and debug builds actively vary it
|
|
// between ranges to keep callers from depending on it.
|
|
namespace Carbon::RawHashtable {
|
|
|
|
// Which prefetch strategies to enable can be controlled via macros to enable
|
|
// doing experiments.
|
|
//
|
|
// Currently, benchmarking on both modern AMD and ARM CPUs seems to indicate
|
|
// that the entry group prefetching is more beneficial than metadata, but that
|
|
// benefit is degraded when enabling them both. This determined our current
|
|
// default of no metadata prefetch but enabled entry group prefetch.
|
|
//
|
|
// Override these by defining them as part of the build explicitly to either `0`
|
|
// or `1`. If left undefined, the defaults will be supplied.
|
|
#ifndef CARBON_ENABLE_PREFETCH_METADATA
|
|
#define CARBON_ENABLE_PREFETCH_METADATA 0
|
|
#endif
|
|
#ifndef CARBON_ENABLE_PREFETCH_ENTRY_GROUP
|
|
#define CARBON_ENABLE_PREFETCH_ENTRY_GROUP 1
|
|
#endif
|
|
|
|
// If allocating storage, allocate a minimum of one cacheline of group metadata
|
|
// or a minimum of one group, whichever is larger.
|
|
inline constexpr ssize_t MinAllocatedSize = std::max<ssize_t>(64, MaxGroupSize);
|
|
|
|
// An entry in the hashtable storage of a `KeyT` and `ValueT` object.
|
|
//
|
|
// Allows manual construction, destruction, and access to these values so we can
|
|
// create arrays of the entries prior to populating them with actual keys and
|
|
// values.
|
|
template <typename KeyT, typename ValueT>
|
|
struct StorageEntry {
|
|
static constexpr bool IsTriviallyDestructible =
|
|
std::is_trivially_destructible_v<KeyT> &&
|
|
std::is_trivially_destructible_v<ValueT>;
|
|
|
|
static constexpr bool IsTriviallyRelocatable =
|
|
IsTriviallyDestructible && std::is_trivially_move_constructible_v<KeyT> &&
|
|
std::is_trivially_move_constructible_v<ValueT>;
|
|
|
|
static constexpr bool IsCopyable =
|
|
IsTriviallyRelocatable || (std::is_copy_constructible_v<KeyT> &&
|
|
std::is_copy_constructible_v<ValueT>);
|
|
|
|
// How iteration refers to an entry, and the iterator traits that follow.
|
|
//
|
|
// The key and value are stored side by side with nothing combining them, so
|
|
// a reference to an entry is a pair of references built on demand. That pair
|
|
// is a *proxy* reference: C++20 forward iterators permit one, but C++17
|
|
// algorithms may assume a forward iterator's reference is a real lvalue, so
|
|
// the C++17 category is `input`.
|
|
using RefT = std::pair<KeyT&, ValueT&>;
|
|
using IterValueT = RefT;
|
|
using IterPointerT = const RefT*;
|
|
using IterCategoryT = std::input_iterator_tag;
|
|
|
|
auto ref() -> RefT { return RefT(key(), value()); }
|
|
|
|
auto key() const -> const KeyT& {
|
|
// Ensure we don't need more alignment than available. Inside a method body
|
|
// to apply to the complete type.
|
|
static_assert(
|
|
alignof(StorageEntry) <= MinAllocatedSize,
|
|
"The minimum allocated size turns into the alignment of our array of "
|
|
"storage entries as they follow the metadata byte array.");
|
|
|
|
return *std::launder(reinterpret_cast<const KeyT*>(&key_storage));
|
|
}
|
|
auto key() -> KeyT& {
|
|
return const_cast<KeyT&>(const_cast<const StorageEntry*>(this)->key());
|
|
}
|
|
|
|
auto value() const -> const ValueT& {
|
|
return *std::launder(reinterpret_cast<const ValueT*>(&value_storage));
|
|
}
|
|
auto value() -> ValueT& {
|
|
return const_cast<ValueT&>(const_cast<const StorageEntry*>(this)->value());
|
|
}
|
|
|
|
// We handle destruction and move manually as we only want to expose distinct
|
|
// `KeyT` and `ValueT` subobjects to user code that may need to do in-place
|
|
// construction. As a consequence, this struct only provides the storage and
|
|
// we have to manually manage the construction, move, and destruction of the
|
|
// objects.
|
|
//
|
|
// Destroys the key and value behind an entry reference. Iteration hands back
|
|
// `RefT` rather than the entry, so this is how a walked entry is destroyed.
|
|
static auto DestroyRef(RefT ref) -> void {
|
|
ref.first.~KeyT();
|
|
ref.second.~ValueT();
|
|
}
|
|
|
|
// Destroys the key and value of this entry. The common case is destroying an
|
|
// entry found in the table's storage, where there is no reference to hand to
|
|
// `DestroyRef`.
|
|
auto Destroy() -> void {
|
|
static_assert(!IsTriviallyDestructible,
|
|
"Should never instantiate when trivial!");
|
|
DestroyRef(ref());
|
|
}
|
|
|
|
auto CopyFrom(const StorageEntry& entry) -> void {
|
|
if constexpr (IsTriviallyRelocatable) {
|
|
memcpy(this, &entry, sizeof(StorageEntry));
|
|
} else {
|
|
new (&key_storage) KeyT(entry.key());
|
|
new (&value_storage) ValueT(entry.value());
|
|
}
|
|
}
|
|
|
|
// Move from an expiring entry and destroy that entry's key and value.
|
|
// Optimizes to directly use `memcpy` when correct.
|
|
auto MoveFrom(StorageEntry&& entry) -> void {
|
|
if constexpr (IsTriviallyRelocatable) {
|
|
memcpy(this, &entry, sizeof(StorageEntry));
|
|
} else {
|
|
new (&key_storage) KeyT(std::move(entry.key()));
|
|
entry.key().~KeyT();
|
|
new (&value_storage) ValueT(std::move(entry.value()));
|
|
entry.value().~ValueT();
|
|
}
|
|
}
|
|
|
|
alignas(KeyT) std::byte key_storage[sizeof(KeyT)];
|
|
alignas(ValueT) std::byte value_storage[sizeof(ValueT)];
|
|
};
|
|
|
|
// A specialization of the storage entry for sets without a distinct value type.
|
|
// Somewhat duplicative with the key-value version, but C++ specialization makes
|
|
// doing better difficult.
|
|
template <typename KeyT>
|
|
struct StorageEntry<KeyT, void> {
|
|
static constexpr bool IsTriviallyDestructible =
|
|
std::is_trivially_destructible_v<KeyT>;
|
|
|
|
static constexpr bool IsTriviallyRelocatable =
|
|
IsTriviallyDestructible && std::is_trivially_move_constructible_v<KeyT>;
|
|
|
|
static constexpr bool IsCopyable =
|
|
IsTriviallyRelocatable || std::is_copy_constructible_v<KeyT>;
|
|
|
|
// As above, but a set's entry is nothing but its key, so a reference to an
|
|
// entry is a true lvalue reference and the iterator is a plain forward one.
|
|
using RefT = KeyT&;
|
|
using IterValueT = std::remove_cv_t<KeyT>;
|
|
using IterPointerT = KeyT*;
|
|
using IterCategoryT = std::forward_iterator_tag;
|
|
|
|
auto ref() -> RefT { return key(); }
|
|
|
|
auto key() const -> const KeyT& {
|
|
// Ensure we don't need more alignment than available.
|
|
static_assert(
|
|
alignof(StorageEntry) <= MinAllocatedSize,
|
|
"The minimum allocated size turns into the alignment of our array of "
|
|
"storage entries as they follow the metadata byte array.");
|
|
|
|
return *std::launder(reinterpret_cast<const KeyT*>(&key_storage));
|
|
}
|
|
auto key() -> KeyT& {
|
|
return const_cast<KeyT&>(const_cast<const StorageEntry*>(this)->key());
|
|
}
|
|
|
|
static auto DestroyRef(RefT ref) -> void { ref.~KeyT(); }
|
|
|
|
auto Destroy() -> void {
|
|
static_assert(!IsTriviallyDestructible,
|
|
"Should never instantiate when trivial!");
|
|
DestroyRef(ref());
|
|
}
|
|
|
|
auto CopyFrom(const StorageEntry& entry) -> void
|
|
requires(IsCopyable)
|
|
{
|
|
if constexpr (IsTriviallyRelocatable) {
|
|
memcpy(this, &entry, sizeof(StorageEntry));
|
|
} else {
|
|
new (&key_storage) KeyT(entry.key());
|
|
}
|
|
}
|
|
|
|
auto MoveFrom(StorageEntry&& entry) -> void {
|
|
if constexpr (IsTriviallyRelocatable) {
|
|
memcpy(this, &entry, sizeof(StorageEntry));
|
|
} else {
|
|
new (&key_storage) KeyT(std::move(entry.key()));
|
|
entry.key().~KeyT();
|
|
}
|
|
}
|
|
|
|
alignas(KeyT) std::byte key_storage[sizeof(KeyT)];
|
|
};
|
|
|
|
struct Metrics {
|
|
// How many keys are present in the table.
|
|
ssize_t key_count = 0;
|
|
// How many slots of the table are reserved due to deleted markers required to
|
|
// preserve probe sequences.
|
|
ssize_t deleted_count = 0;
|
|
// How many bytes of allocated storage are used by the table. Note, does not
|
|
// include the table object or any small-size buffer.
|
|
ssize_t storage_bytes = 0;
|
|
|
|
// How many keys have required probing beyond the initial group. These are the
|
|
// keys with a probe distance > 0.
|
|
ssize_t probed_key_count = 0;
|
|
// The probe distance averaged over every key. If every key is in its initial
|
|
// group, this will be zero as no keys will have a larger probe distance. In
|
|
// general, we want this to be as close to zero as possible.
|
|
double probe_avg_distance = 0.0;
|
|
// The maximum probe distance found for a single key in the table.
|
|
ssize_t probe_max_distance = 0;
|
|
// The average number of probing comparisons required to locate a specific key
|
|
// in the table. This is how many comparisons are required *before* the key is
|
|
// located, or the *failed* comparisons. We always have to do one successful
|
|
// comparison at the end. This successful comparison isn't counted because
|
|
// that focuses this metric on the overhead the table is introducing, and
|
|
// keeps a "perfect" table with an average of `0.0` here similar to the
|
|
// perfect average of `0.0` average probe distance.
|
|
double probe_avg_compares = 0.0;
|
|
// The maximum number of probing comparisons required to locate a specific
|
|
// key in the table.
|
|
ssize_t probe_max_compares = 0;
|
|
};
|
|
|
|
// A placeholder empty type used to model pointers to the allocated buffer of
|
|
// storage.
|
|
//
|
|
// The allocated storage doesn't have a meaningful static layout -- it consists
|
|
// of an array of metadata groups followed by an array of storage entries.
|
|
// However, we want to be able to mark pointers to this and so use pointers to
|
|
// this placeholder type as that signifier.
|
|
//
|
|
// This is a complete, empty type so that it can be used as a base class of a
|
|
// specific concrete storage type for compile-time sized storage.
|
|
struct Storage {};
|
|
|
|
// Forward declaration to support friending, see the definition below.
|
|
template <typename InputKeyT, typename InputValueT, typename InputKeyContextT>
|
|
class BaseImpl;
|
|
|
|
// Implementation helper for defining a read-only view type for a hashtable.
|
|
//
|
|
// A specific user-facing hashtable view type should derive privately from this
|
|
// type, and forward the implementation of its interface to functions in this
|
|
// type.
|
|
//
|
|
// The methods available to user-facing hashtable types are `protected`, and
|
|
// where they are expected to directly map to a public API, named with an
|
|
// `Impl`. The suffix naming ensures types don't `using` in these low-level APIs
|
|
// but declare their own and implement them by forwarding to these APIs. We
|
|
// don't want users to have to read these implementation details to understand
|
|
// their container's API, so none of these methods should be `using`-ed into the
|
|
// user facing types.
|
|
//
|
|
// Some of the types are just convenience aliases and aren't important to
|
|
// surface as part of the user-facing type API for readers and so those are
|
|
// reasonable to add via a `using`.
|
|
//
|
|
// Some methods are used by other parts of the raw hashtable implementation.
|
|
// Those are kept `private` and where necessary the other components of the raw
|
|
// hashtable implementation are friended to give access to them.
|
|
template <typename InputKeyT, typename InputValueT, typename InputKeyContextT>
|
|
class ViewImpl {
|
|
protected:
|
|
using KeyT = InputKeyT;
|
|
using ValueT = InputValueT;
|
|
using KeyContextT = InputKeyContextT;
|
|
using EntryT = StorageEntry<KeyT, ValueT>;
|
|
using MetricsT = Metrics;
|
|
|
|
// What iterating over the table's entries produces: a `KeyT&` for a set, and
|
|
// a `std::pair<KeyT&, ValueT&>` for a map. See `StorageEntry`.
|
|
using EntryRefT = EntryT::RefT;
|
|
|
|
// The range type produced by `EntriesImpl`.
|
|
class EntryRange;
|
|
|
|
friend class BaseImpl<KeyT, ValueT, KeyContextT>;
|
|
template <typename InputBaseT, ssize_t SmallSize>
|
|
friend class TableImpl;
|
|
|
|
// Make more-`const` types friends to enable conversions that add `const`.
|
|
friend class ViewImpl<const KeyT, ValueT, KeyContextT>;
|
|
friend class ViewImpl<KeyT, const ValueT, KeyContextT>;
|
|
friend class ViewImpl<const KeyT, const ValueT, KeyContextT>;
|
|
|
|
ViewImpl() = default;
|
|
|
|
// Support adding `const` to either key or value type of some other view.
|
|
template <typename OtherKeyT, typename OtherValueT>
|
|
explicit(false)
|
|
ViewImpl(ViewImpl<OtherKeyT, OtherValueT, KeyContextT> other_view)
|
|
requires(SameAsOneOf<KeyT, OtherKeyT, const OtherKeyT> &&
|
|
SameAsOneOf<ValueT, OtherValueT, const OtherValueT>)
|
|
: alloc_size_(other_view.alloc_size_), storage_(other_view.storage_) {}
|
|
|
|
// Looks up an entry in the hashtable and returns its address or null if not
|
|
// present.
|
|
template <typename LookupKeyT>
|
|
auto LookupEntry(LookupKeyT lookup_key, KeyContextT key_context) const
|
|
-> EntryT*;
|
|
|
|
// Returns a range for iterating over all entries in the hashtable.
|
|
//
|
|
// The returned range copies this view, so it remains valid for as long as the
|
|
// underlying table does, independent of this view's lifetime.
|
|
auto EntriesImpl() const -> EntryRange;
|
|
|
|
// Returns a collection of informative metrics on the the current state of the
|
|
// table, useful for performance analysis. These include relatively slow to
|
|
// compute metrics requiring deep inspection of the table's state.
|
|
auto ComputeMetricsImpl(KeyContextT key_context) const -> MetricsT;
|
|
|
|
private:
|
|
ViewImpl(ssize_t alloc_size, Storage* storage)
|
|
: alloc_size_(alloc_size), storage_(storage) {}
|
|
|
|
// Computes the offset from the metadata array to the entries array for a
|
|
// given size. This is trivial, but we use this routine to enforce invariants
|
|
// on the sizes.
|
|
static constexpr auto EntriesOffset(ssize_t alloc_size) -> ssize_t {
|
|
CARBON_DCHECK(llvm::isPowerOf2_64(alloc_size),
|
|
"Size must be a power of two for a hashed buffer!");
|
|
// The size is always a power of two. We prevent any too-small sizes so it
|
|
// being a power of two provides the needed alignment. As a result, the
|
|
// offset is exactly the size. We validate this here to catch alignment bugs
|
|
// early.
|
|
CARBON_DCHECK(static_cast<uint64_t>(alloc_size) ==
|
|
llvm::alignTo<alignof(EntryT)>(alloc_size));
|
|
return alloc_size;
|
|
}
|
|
|
|
// Compute the allocated table's byte size.
|
|
static constexpr auto AllocByteSize(ssize_t alloc_size) -> ssize_t {
|
|
return EntriesOffset(alloc_size) + sizeof(EntryT) * alloc_size;
|
|
}
|
|
|
|
auto metadata() const -> uint8_t* {
|
|
return reinterpret_cast<uint8_t*>(storage_);
|
|
}
|
|
auto entries_data() const -> EntryT* {
|
|
return reinterpret_cast<EntryT*>(reinterpret_cast<std::byte*>(storage_) +
|
|
EntriesOffset(alloc_size_));
|
|
}
|
|
|
|
// Prefetch the metadata prior to probing. This is to overlap any of the
|
|
// memory access latency we can with the hashing of a key or other
|
|
// latency-bound operation prior to probing.
|
|
auto PrefetchMetadata() const -> void {
|
|
if constexpr (CARBON_ENABLE_PREFETCH_METADATA) {
|
|
// Prefetch with a "low" temporal locality as we're primarily expecting a
|
|
// brief use of the metadata and then to return to application code.
|
|
__builtin_prefetch(metadata(), /*read*/ 0, /*low-locality*/ 1);
|
|
}
|
|
}
|
|
|
|
// Prefetch an entry. This prefetches for read as it is primarily expected to
|
|
// be used in the probing path, and writing afterwards isn't especially slowed
|
|
// down. We don't want to synthesize writes unless we *know* we're going to
|
|
// write.
|
|
static auto PrefetchEntryGroup(const EntryT* entry_group) -> void {
|
|
if constexpr (CARBON_ENABLE_PREFETCH_ENTRY_GROUP) {
|
|
// Prefetch with a "low" temporal locality as we're primarily expecting a
|
|
// brief use of the entries and then to return to application code.
|
|
__builtin_prefetch(entry_group, /*read*/ 0, /*low-locality*/ 1);
|
|
}
|
|
}
|
|
|
|
ssize_t alloc_size_;
|
|
Storage* storage_;
|
|
};
|
|
|
|
// A range over the entries of a hashtable.
|
|
//
|
|
// A dedicated range object is used rather than a plain pair of iterators (such
|
|
// as `llvm::iterator_range`) because the range scopes two debug-only behaviors
|
|
// that a bare iterator pair has nowhere to store:
|
|
//
|
|
// - Mutation checking: the range snapshots a hash of the table's metadata on
|
|
// construction and re-checks it on destruction, catching tables that were
|
|
// mutated while iteration was active.
|
|
// - Traversal order: the group at which iteration starts, and the stride it
|
|
// walks the groups with, are drawn from an entropy pool once when the range
|
|
// is constructed.
|
|
// Deriving them here rather than in `begin()` keeps `begin()` a pure function
|
|
// of the range so that it can be called repeatedly, as forward ranges
|
|
// require, while still varying the order between separately created ranges.
|
|
//
|
|
// The range holds the view *by value*; views are two words and designed to be
|
|
// cheap to copy. It deliberately does not point back at the view it was created
|
|
// from, as views are routinely temporaries or by-value parameters whose
|
|
// lifetime is shorter than the table they refer to.
|
|
//
|
|
// This type provides only the minimal `begin()` and `end()` interface needed by
|
|
// range-based for loops and the range concepts, which also avoids any
|
|
// compile-time cost from including `<ranges>`.
|
|
template <typename InputKeyT, typename InputValueT, typename InputKeyContextT>
|
|
class ViewImpl<InputKeyT, InputValueT, InputKeyContextT>::EntryRange {
|
|
public:
|
|
class Iterator;
|
|
|
|
using value_type = typename EntryT::IterValueT;
|
|
using reference = EntryRefT;
|
|
using difference_type = ssize_t;
|
|
|
|
explicit EntryRange(ViewImpl view);
|
|
|
|
// Copyable: every member is a scalar snapshot of the table. Copying a range
|
|
// in a debug build simply validates the same table state more than once.
|
|
EntryRange(const EntryRange&) = default;
|
|
auto operator=(const EntryRange&) -> EntryRange& = default;
|
|
|
|
#ifndef NDEBUG
|
|
// Only debug builds declare a destructor, and so only they re-check the
|
|
// table on the way out. Release builds leave the range trivially
|
|
// destructible, and so trivial for the purposes of calls, letting it be
|
|
// passed and returned in registers.
|
|
~EntryRange() { CheckInvariants(); }
|
|
#endif
|
|
|
|
auto begin() const -> Iterator;
|
|
auto end() const -> Iterator;
|
|
|
|
private:
|
|
// The facade `Iterator` derives from. A class can't name one of its own
|
|
// aliases in its base-specifier, so naming it here lets `Iterator` spell it
|
|
// once instead of repeating it to get at the members it inherits.
|
|
using IteratorBase =
|
|
llvm::iterator_facade_base<Iterator, typename EntryT::IterCategoryT,
|
|
value_type, difference_type,
|
|
typename EntryT::IterPointerT, reference>;
|
|
|
|
#ifndef NDEBUG
|
|
// Checks that the table's metadata has not changed since construction.
|
|
auto CheckInvariants() const -> void;
|
|
#endif
|
|
|
|
ViewImpl view_;
|
|
#ifndef NDEBUG
|
|
HashCode initial_metadata_hash_ = {};
|
|
ssize_t start_group_ = 0;
|
|
ssize_t step_ = GroupSize;
|
|
#endif
|
|
};
|
|
|
|
// Two-level forward iterator through present hashtable entries.
|
|
template <typename InputKeyT, typename InputValueT, typename InputKeyContextT>
|
|
class ViewImpl<InputKeyT, InputValueT, InputKeyContextT>::EntryRange::Iterator
|
|
: public EntryRange::IteratorBase {
|
|
public:
|
|
// Both the set and map forms satisfy C++20's `std::forward_iterator`. A
|
|
// map's `reference` is a proxy, which pins its C++17 `iterator_category` to
|
|
// `input`, but the C++20 concept is unaffected. See `EntryRefT`.
|
|
using iterator_concept = std::forward_iterator_tag;
|
|
|
|
Iterator() = default;
|
|
|
|
using EntryRange::IteratorBase::operator++;
|
|
|
|
[[clang::always_inline]] auto operator*() const -> EntryRefT {
|
|
CARBON_DCHECK(present_bits_ != 0, "Dereferencing end iterator!");
|
|
__builtin_assume(present_bits_ != 0);
|
|
// `index_ptr` folds scaling the match index by the entry size together
|
|
// with decoding the index itself, which saves a shift on the portable
|
|
// byte-encoded code path.
|
|
return MatchIndex(present_bits_).index_ptr(group_entries())->ref();
|
|
}
|
|
|
|
[[clang::always_inline]] auto operator++() -> Iterator& {
|
|
CARBON_DCHECK(present_bits_ != 0, "Incrementing end iterator!");
|
|
__builtin_assume(present_bits_ != 0);
|
|
present_bits_ &= (present_bits_ - 1);
|
|
if (LLVM_LIKELY(present_bits_ != 0)) {
|
|
return *this;
|
|
}
|
|
AdvanceToNextPresentGroup();
|
|
return *this;
|
|
}
|
|
|
|
friend auto operator==(const Iterator& lhs, const Iterator& rhs) -> bool {
|
|
if (lhs.present_bits_ == 0 || rhs.present_bits_ == 0) {
|
|
return lhs.present_bits_ == rhs.present_bits_;
|
|
}
|
|
// The entry pointer already encodes the base and the group offset, so it
|
|
// uniquely identifies the group without a separate index.
|
|
return lhs.group_entries() == rhs.group_entries() &&
|
|
lhs.present_bits_ == rhs.present_bits_;
|
|
}
|
|
|
|
private:
|
|
friend class EntryRange;
|
|
|
|
using MatchBitsT = typename MetadataGroup::MatchPresentRange::BitsT;
|
|
using MatchIndex = typename MetadataGroup::MatchIndex;
|
|
|
|
// Builds an iterator to the first present entry of `range`, or an iterator
|
|
// equal to `end()` when the range has no entries to walk. The parameters of
|
|
// the walk differ between builds, so both are drawn from the range here
|
|
// rather than passed in.
|
|
[[clang::always_inline]] explicit Iterator(const EntryRange& range);
|
|
|
|
[[clang::always_inline]] auto AdvanceToNextPresentGroup() -> void;
|
|
|
|
// The entries of the group the iterator is currently within. Both builds
|
|
// track the current group, but they encode it differently, so the encoding
|
|
// is hidden behind this accessor.
|
|
auto group_entries() const -> EntryT* {
|
|
#ifndef NDEBUG
|
|
return group_entries_;
|
|
#else
|
|
return entries_end_ + group_offset_;
|
|
#endif
|
|
}
|
|
|
|
#ifndef NDEBUG
|
|
// Debug builds walk groups in a randomized order and so must retain the
|
|
// array bases along with the parameters of the walk. The randomized walk
|
|
// revisits no group but also never reaches the end of the array, so it does
|
|
// need an explicit count of the groups left to visit.
|
|
EntryT* group_entries_ = nullptr;
|
|
const uint8_t* metadata_ = nullptr;
|
|
EntryT* entries_ = nullptr;
|
|
ssize_t groups_remaining_ = 0;
|
|
ssize_t group_index_ = 0;
|
|
size_t probe_mask_ = 0;
|
|
ssize_t step_ = GroupSize;
|
|
#else
|
|
// Release builds walk the groups in order, tracking the position as a
|
|
// *negative* byte offset from the end of each array that counts up to zero.
|
|
// Anchoring at the ends rather than the beginnings means the walk needs only
|
|
// this one induction variable, and reaching zero is the bound.
|
|
EntryT* entries_end_ = nullptr;
|
|
const uint8_t* metadata_end_ = nullptr;
|
|
ssize_t group_offset_ = 0;
|
|
#endif
|
|
MatchBitsT present_bits_ = 0;
|
|
};
|
|
|
|
// Implementation helper for defining a read-write base type for a hashtable
|
|
// that type-erases any SSO buffer.
|
|
//
|
|
// A specific user-facing hashtable base type should derive using *`protected`*
|
|
// inheritance from this type, and forward the implementation of its interface
|
|
// to functions in this type.
|
|
//
|
|
// Other than the use of `protected` inheritance, the patterns for this type,
|
|
// and how to build user-facing hashtable base types from it, mirror those of
|
|
// `ViewImpl`. See its documentation for more details.
|
|
template <typename InputKeyT, typename InputValueT, typename InputKeyContextT>
|
|
class BaseImpl {
|
|
protected:
|
|
using KeyT = InputKeyT;
|
|
using ValueT = InputValueT;
|
|
using KeyContextT = InputKeyContextT;
|
|
using ViewImplT = ViewImpl<KeyT, ValueT, KeyContextT>;
|
|
using EntryT = ViewImplT::EntryT;
|
|
using MetricsT = ViewImplT::MetricsT;
|
|
|
|
BaseImpl(int small_alloc_size, Storage* small_storage)
|
|
: small_alloc_size_(small_alloc_size) {
|
|
CARBON_CHECK(small_alloc_size >= 0);
|
|
Construct(small_storage);
|
|
}
|
|
// Only used for copying and moving, and leaves storage uninitialized.
|
|
BaseImpl(ssize_t alloc_size, int growth_budget, int small_alloc_size)
|
|
: view_impl_(alloc_size, nullptr),
|
|
growth_budget_(growth_budget),
|
|
small_alloc_size_(small_alloc_size) {}
|
|
|
|
// Destruction must be handled by the table where it can destroy entries in
|
|
// any small buffer, so make the base destructor protected but defaulted here.
|
|
~BaseImpl() = default;
|
|
|
|
// NOLINTNEXTLINE(google-explicit-constructor): Designed to implicitly decay.
|
|
explicit(false) operator ViewImplT() const { return view_impl(); }
|
|
|
|
auto view_impl() const -> const ViewImplT& { return view_impl_; }
|
|
|
|
// Destroys all non-trivially destructible entries in the table.
|
|
auto DestroyEntries() -> void;
|
|
|
|
// Looks up the provided key in the hashtable. If found, returns a pointer to
|
|
// that entry and `false`.
|
|
//
|
|
// If not found, will locate an empty entry for inserting into, set the
|
|
// metadata for that entry, and return a pointer to the entry and `true`. When
|
|
// necessary, this will grow the hashtable to cause there to be sufficient
|
|
// empty entries.
|
|
template <typename LookupKeyT>
|
|
auto InsertImpl(LookupKeyT lookup_key, KeyContextT key_context)
|
|
-> std::pair<EntryT*, bool>;
|
|
|
|
// Grow the table to specific allocation size.
|
|
//
|
|
// This will grow the table if necessary for it to have an allocation size
|
|
// of `target_alloc_size` which must be a power of two. Note that this will
|
|
// not allow that many keys to be inserted into the hashtable, but a smaller
|
|
// number based on the load factor. If a specific number of insertions need to
|
|
// be achieved without triggering growth, use the `GrowForInsertCountImpl`
|
|
// method.
|
|
auto GrowToAllocSizeImpl(ssize_t target_alloc_size, KeyContextT key_context)
|
|
-> void;
|
|
|
|
// Grow the table to allow inserting the specified number of keys.
|
|
auto GrowForInsertCountImpl(ssize_t count, KeyContextT key_context) -> void;
|
|
|
|
// Looks up the entry in the hashtable, and if found destroys the entry and
|
|
// returns `true`. If not found, returns `false`.
|
|
//
|
|
// Does not release any memory, just leaves a tombstone behind so this entry
|
|
// cannot be found and the slot can in theory be reused.
|
|
template <typename LookupKeyT>
|
|
auto EraseImpl(LookupKeyT lookup_key, KeyContextT key_context) -> bool;
|
|
|
|
// Erases all entries in the hashtable but leaves the allocated storage.
|
|
auto ClearImpl() -> void;
|
|
|
|
private:
|
|
template <typename InputBaseT, ssize_t SmallSize>
|
|
friend class TableImpl;
|
|
|
|
static constexpr ssize_t Alignment = std::max<ssize_t>(
|
|
alignof(MetadataGroup), alignof(StorageEntry<KeyT, ValueT>));
|
|
|
|
// Implementation of inline small storage for the provided key type, value
|
|
// type, and small size. Specialized for a zero small size to be an empty
|
|
// struct.
|
|
template <ssize_t SmallSize>
|
|
struct SmallStorage : Storage {
|
|
alignas(Alignment) uint8_t metadata[SmallSize];
|
|
mutable StorageEntry<KeyT, ValueT> entries[SmallSize];
|
|
};
|
|
// Specialized storage with no inline buffer to avoid any extra alignment.
|
|
template <>
|
|
struct SmallStorage<0> {};
|
|
|
|
static auto Allocate(ssize_t alloc_size) -> Storage*;
|
|
static auto Deallocate(Storage* storage, ssize_t alloc_size) -> void;
|
|
|
|
auto growth_budget() const -> ssize_t { return growth_budget_; }
|
|
auto alloc_size() const -> ssize_t { return view_impl_.alloc_size_; }
|
|
auto alloc_size() -> ssize_t& { return view_impl_.alloc_size_; }
|
|
auto storage() const -> Storage* { return view_impl_.storage_; }
|
|
auto storage() -> Storage*& { return view_impl_.storage_; }
|
|
auto metadata() const -> uint8_t* { return view_impl_.metadata(); }
|
|
auto entries_data() const -> EntryT* { return view_impl_.entries_data(); }
|
|
auto small_alloc_size() const -> ssize_t {
|
|
return static_cast<unsigned>(small_alloc_size_);
|
|
}
|
|
auto is_small() const -> bool {
|
|
CARBON_DCHECK(alloc_size() >= small_alloc_size());
|
|
return alloc_size() == small_alloc_size();
|
|
}
|
|
|
|
// Wrapper to call `ViewImplT::PrefetchStorage`, see that method for details.
|
|
auto PrefetchStorage() const -> void { view_impl_.PrefetchMetadata(); }
|
|
|
|
auto Construct(Storage* small_storage) -> void;
|
|
auto Destroy() -> void;
|
|
auto CopySlotsFrom(const BaseImpl& arg) -> void
|
|
requires(EntryT::IsCopyable);
|
|
auto MoveFrom(BaseImpl&& arg, Storage* small_storage) -> void;
|
|
|
|
auto InsertIntoEmpty(HashCode hash) -> EntryT*;
|
|
|
|
static auto ComputeNextAllocSize(ssize_t old_alloc_size) -> ssize_t;
|
|
static auto GrowthThresholdForAllocSize(ssize_t alloc_size) -> ssize_t;
|
|
|
|
auto GrowToNextAllocSize(KeyContextT key_context) -> void;
|
|
auto GrowAndInsert(HashCode hash, KeyContextT key_context) -> EntryT*;
|
|
|
|
ViewImplT view_impl_;
|
|
int growth_budget_;
|
|
int small_alloc_size_;
|
|
};
|
|
|
|
// Implementation helper for defining a hashtable type with an SSO buffer.
|
|
//
|
|
// A specific user-facing hashtable should derive privately from this
|
|
// type, and forward the implementation of its interface to functions in this
|
|
// type. It should provide the corresponding user-facing hashtable base type as
|
|
// the `InputBaseT` type parameter (rather than a key/value pair), and this type
|
|
// will in turn derive from that provided base type. This allows derived-to-base
|
|
// conversion from the user-facing hashtable type to the user-facing hashtable
|
|
// base type. And it does so keeping the inheritance linear. The resulting
|
|
// linear inheritance hierarchy for a `Map<K, T>` type will look like:
|
|
//
|
|
// Map<K, T>
|
|
// ↓
|
|
// TableImpl<MapBase<K, T>>
|
|
// ↓
|
|
// MapBase<K, T>
|
|
// ↓
|
|
// BaseImpl<K, T>
|
|
//
|
|
// Other than this inheritance technique, the patterns for this type, and how to
|
|
// build user-facing hashtable types from it, mirror those of `ViewImpl`. See
|
|
// its documentation for more details.
|
|
template <typename InputBaseT, ssize_t SmallSize>
|
|
class TableImpl : public InputBaseT {
|
|
protected:
|
|
using BaseT = InputBaseT;
|
|
|
|
TableImpl() : BaseT(SmallSize, small_storage()) {}
|
|
TableImpl(const TableImpl& arg)
|
|
requires(BaseT::EntryT::IsCopyable);
|
|
TableImpl(TableImpl&& arg) noexcept;
|
|
auto operator=(const TableImpl& arg) -> TableImpl&
|
|
requires(BaseT::EntryT::IsCopyable);
|
|
auto operator=(TableImpl&& arg) noexcept -> TableImpl&;
|
|
~TableImpl();
|
|
|
|
// Resets the hashtable to its initial state, clearing all entries and
|
|
// releasing all memory. If the hashtable had an SSO buffer, that is restored
|
|
// as the storage. Otherwise, a minimum sized table storage is allocated.
|
|
auto ResetImpl() -> void;
|
|
|
|
private:
|
|
using KeyT = BaseT::KeyT;
|
|
using ValueT = BaseT::ValueT;
|
|
using EntryT = BaseT::EntryT;
|
|
using SmallStorage = BaseT::template SmallStorage<SmallSize>;
|
|
|
|
auto small_storage() const -> Storage*;
|
|
|
|
auto SetUpStorage() -> void;
|
|
|
|
[[no_unique_address]] mutable SmallStorage small_storage_;
|
|
};
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Only implementation details below this point.
|
|
//
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Computes a seed that provides a small amount of entropy from ASLR where
|
|
// available with minimal cost. The priority is speed, and this computes the
|
|
// entropy in a way that doesn't require loading from memory, merely accessing
|
|
// entropy already available without accessing memory.
|
|
inline auto ComputeSeed() -> uint64_t {
|
|
// A global variable whose address is used as a seed. This allows ASLR to
|
|
// introduce some variation in hashtable ordering when enabled via the code
|
|
// model for globals.
|
|
extern volatile std::byte global_addr_seed;
|
|
|
|
return reinterpret_cast<uint64_t>(&global_addr_seed);
|
|
}
|
|
|
|
#ifndef NDEBUG
|
|
// A pool of entropy used to vary the iteration order of hashtables in debug
|
|
// builds. It is seeded from ASLR where available.
|
|
extern std::atomic<HashCode> entropy_hash;
|
|
|
|
// Returns a pseudo-random value from the entropy pool, advancing the pool.
|
|
//
|
|
// The load and store are separate relaxed operations rather than one atomic
|
|
// read-modify-write so that consuming entropy is just a load, and refreshing
|
|
// the pool doesn't block the iteration that follows. Racing callers can lose an
|
|
// update and draw the same value, which is fine for a debug aid.
|
|
inline auto NextRangeEntropy() -> HashCode {
|
|
HashCode prev_entropy_hash = entropy_hash.load(std::memory_order_relaxed);
|
|
entropy_hash.store(Carbon::HashValue(prev_entropy_hash),
|
|
std::memory_order_relaxed);
|
|
return prev_entropy_hash;
|
|
}
|
|
#endif
|
|
|
|
inline auto ComputeProbeMaskFromSize(ssize_t size) -> size_t {
|
|
CARBON_DCHECK(llvm::isPowerOf2_64(size),
|
|
"Size must be a power of two for a hashed buffer!");
|
|
// Since `size` is a power of two, we can make sure the probes are less
|
|
// than `size` by making the mask `size - 1`. We also mask off the low
|
|
// bits so the probes are a multiple of the size of the groups of entries.
|
|
return (size - 1) & ~GroupMask;
|
|
}
|
|
|
|
// This class handles building a sequence of probe indices from a given
|
|
// starting point, including both the quadratic growth and masking the index
|
|
// to stay within the bucket array size. The starting point doesn't need to be
|
|
// clamped to the size ahead of time (or even be positive), we will do it
|
|
// internally.
|
|
//
|
|
// For reference on quadratic probing:
|
|
// https://en.wikipedia.org/wiki/Quadratic_probing
|
|
//
|
|
// We compute the quadratic probe index incrementally, but we can also compute
|
|
// it mathematically and will check that the incremental result matches our
|
|
// mathematical expectation. We use the quadratic probing formula of:
|
|
//
|
|
// p(start, step) = (start + (step + step^2) / 2) (mod size / GroupSize)
|
|
//
|
|
// However, we compute it incrementally and scale all the variables by the group
|
|
// size so it can be used as an index without an additional multiplication.
|
|
class ProbeSequence {
|
|
public:
|
|
ProbeSequence(ssize_t start, ssize_t size) {
|
|
mask_ = ComputeProbeMaskFromSize(size);
|
|
p_ = start & mask_;
|
|
#ifndef NDEBUG
|
|
start_ = start & mask_;
|
|
size_ = size;
|
|
#endif
|
|
}
|
|
|
|
auto Next() -> void {
|
|
step_ += GroupSize;
|
|
p_ = (p_ + step_) & mask_;
|
|
#ifndef NDEBUG
|
|
// Verify against the quadratic formula we expect to be following by scaling
|
|
// everything down by `GroupSize`.
|
|
CARBON_DCHECK(
|
|
(p_ / GroupSize) ==
|
|
((start_ / GroupSize +
|
|
(step_ / GroupSize + (step_ / GroupSize) * (step_ / GroupSize)) /
|
|
2) %
|
|
(size_ / GroupSize)),
|
|
"Index in probe sequence does not match the expected formula.");
|
|
CARBON_DCHECK(step_ < size_,
|
|
"We necessarily visit all groups, so we can't have more "
|
|
"probe steps than groups.");
|
|
#endif
|
|
}
|
|
|
|
auto index() const -> ssize_t { return p_; }
|
|
|
|
private:
|
|
ssize_t step_ = 0;
|
|
size_t mask_;
|
|
ssize_t p_;
|
|
#ifndef NDEBUG
|
|
ssize_t start_;
|
|
ssize_t size_;
|
|
#endif
|
|
};
|
|
|
|
// TODO: Evaluate keeping this outlined to see if macro benchmarks observe the
|
|
// same perf hit as micro benchmarks.
|
|
template <typename InputKeyT, typename InputValueT, typename InputKeyContextT>
|
|
template <typename LookupKeyT>
|
|
auto ViewImpl<InputKeyT, InputValueT, InputKeyContextT>::LookupEntry(
|
|
LookupKeyT lookup_key, KeyContextT key_context) const -> EntryT* {
|
|
PrefetchMetadata();
|
|
|
|
ssize_t local_size = alloc_size_;
|
|
CARBON_DCHECK(local_size > 0);
|
|
|
|
uint8_t* local_metadata = metadata();
|
|
HashCode hash = key_context.HashKey(lookup_key, ComputeSeed());
|
|
auto [hash_index, tag] = hash.ExtractIndexAndTag<7>();
|
|
|
|
EntryT* local_entries = entries_data();
|
|
|
|
// Walk through groups of entries using a quadratic probe starting from
|
|
// `hash_index`.
|
|
ProbeSequence s(hash_index, local_size);
|
|
do {
|
|
ssize_t group_index = s.index();
|
|
|
|
// Load the group's metadata and prefetch the entries for this group. The
|
|
// prefetch here helps hide key access latency while we're matching the
|
|
// metadata.
|
|
MetadataGroup g = MetadataGroup::Load(local_metadata, group_index);
|
|
EntryT* group_entries = &local_entries[group_index];
|
|
PrefetchEntryGroup(group_entries);
|
|
|
|
// For each group, match the tag against the metadata to extract the
|
|
// potentially matching entries within the group.
|
|
auto metadata_matched_range = g.Match(tag);
|
|
if (LLVM_LIKELY(metadata_matched_range)) {
|
|
// If any entries in this group potentially match based on their metadata,
|
|
// walk each candidate and compare its key to see if we have definitively
|
|
// found a match.
|
|
auto byte_it = metadata_matched_range.begin();
|
|
auto byte_end = metadata_matched_range.end();
|
|
do {
|
|
EntryT* entry = byte_it.index_ptr(group_entries);
|
|
if (LLVM_LIKELY(key_context.KeyEq(lookup_key, entry->key()))) {
|
|
__builtin_assume(entry != nullptr);
|
|
return entry;
|
|
}
|
|
++byte_it;
|
|
} while (LLVM_UNLIKELY(byte_it != byte_end));
|
|
}
|
|
|
|
// We failed to find a matching entry in this bucket, so check if there are
|
|
// empty slots as that indicates we're done probing -- no later probed index
|
|
// could have a match.
|
|
auto empty_byte_matched_range = g.MatchEmpty();
|
|
if (LLVM_LIKELY(empty_byte_matched_range)) {
|
|
return nullptr;
|
|
}
|
|
|
|
s.Next();
|
|
|
|
// We use a weird construct of an "unlikely" condition of `true`. The goal
|
|
// is to get the compiler to not prioritize the back edge of the loop for
|
|
// code layout, and in at least some tests this seems to be an effective
|
|
// construct for achieving this.
|
|
} while (LLVM_UNLIKELY(true));
|
|
}
|
|
|
|
template <typename InputKeyT, typename InputValueT, typename InputKeyContextT>
|
|
auto ViewImpl<InputKeyT, InputValueT, InputKeyContextT>::ComputeMetricsImpl(
|
|
KeyContextT key_context) const -> Metrics {
|
|
uint8_t* local_metadata = metadata();
|
|
EntryT* local_entries = entries_data();
|
|
ssize_t local_size = alloc_size_;
|
|
|
|
Metrics metrics;
|
|
|
|
// Compute the ones we can directly.
|
|
metrics.deleted_count = llvm::count(
|
|
llvm::ArrayRef(local_metadata, local_size), MetadataGroup::Deleted);
|
|
metrics.storage_bytes = AllocByteSize(local_size);
|
|
|
|
// We want to process present slots specially to collect metrics on their
|
|
// probing behavior.
|
|
for (ssize_t group_index = 0; group_index < local_size;
|
|
group_index += GroupSize) {
|
|
auto g = MetadataGroup::Load(local_metadata, group_index);
|
|
auto present_matched_range = g.MatchPresent();
|
|
for (ssize_t byte_index : present_matched_range) {
|
|
++metrics.key_count;
|
|
ssize_t index = group_index + byte_index;
|
|
HashCode hash =
|
|
key_context.HashKey(local_entries[index].key(), ComputeSeed());
|
|
auto [hash_index, tag] = hash.ExtractIndexAndTag<7>();
|
|
ProbeSequence s(hash_index, local_size);
|
|
metrics.probed_key_count +=
|
|
static_cast<ssize_t>(s.index() != group_index);
|
|
|
|
// For each probed key, go through the probe sequence to find both the
|
|
// probe distance and how many comparisons are required.
|
|
ssize_t distance = 0;
|
|
ssize_t compares = 0;
|
|
for (; s.index() != group_index; s.Next()) {
|
|
auto probe_g = MetadataGroup::Load(local_metadata, s.index());
|
|
auto probe_matched_range = probe_g.Match(tag);
|
|
compares += std::distance(probe_matched_range.begin(),
|
|
probe_matched_range.end());
|
|
distance += 1;
|
|
}
|
|
|
|
auto probe_g = MetadataGroup::Load(local_metadata, s.index());
|
|
auto probe_matched_range = probe_g.Match(tag);
|
|
CARBON_CHECK(!probe_matched_range.empty());
|
|
for (ssize_t match_index : probe_matched_range) {
|
|
if (match_index >= byte_index) {
|
|
// Note we only count the compares that will *fail* as part of
|
|
// probing. The last successful compare isn't interesting, it is
|
|
// always needed.
|
|
break;
|
|
}
|
|
compares += 1;
|
|
}
|
|
metrics.probe_avg_distance += distance;
|
|
metrics.probe_max_distance =
|
|
std::max(metrics.probe_max_distance, distance);
|
|
metrics.probe_avg_compares += compares;
|
|
metrics.probe_max_compares =
|
|
std::max(metrics.probe_max_compares, compares);
|
|
}
|
|
}
|
|
if (metrics.key_count > 0) {
|
|
metrics.probe_avg_compares /= metrics.key_count;
|
|
metrics.probe_avg_distance /= metrics.key_count;
|
|
}
|
|
return metrics;
|
|
}
|
|
|
|
template <typename InputKeyT, typename InputValueT, typename InputKeyContextT>
|
|
[[clang::always_inline]] auto
|
|
ViewImpl<InputKeyT, InputValueT, InputKeyContextT>::EntriesImpl() const
|
|
-> EntryRange {
|
|
return EntryRange(*this);
|
|
}
|
|
|
|
template <typename InputKeyT, typename InputValueT, typename InputKeyContextT>
|
|
[[clang::always_inline]]
|
|
ViewImpl<InputKeyT, InputValueT, InputKeyContextT>::EntryRange::Iterator::
|
|
Iterator(const EntryRange& range) {
|
|
const ViewImpl& view = range.view_;
|
|
ssize_t alloc_size = view.alloc_size_;
|
|
|
|
// An empty or moved-from table has no groups to load from, and the
|
|
// default-initialized state left behind already compares equal to `end()`.
|
|
if (alloc_size == 0 || view.storage_ == nullptr) {
|
|
return;
|
|
}
|
|
|
|
#ifndef NDEBUG
|
|
entries_ = view.entries_data();
|
|
metadata_ = view.metadata();
|
|
// The starting group and stride were drawn when the range was constructed,
|
|
// so every iterator built from it walks the same order.
|
|
group_index_ = range.start_group_;
|
|
group_entries_ = entries_ + group_index_;
|
|
groups_remaining_ = alloc_size / GroupSize - 1;
|
|
probe_mask_ = ComputeProbeMaskFromSize(alloc_size);
|
|
step_ = range.step_;
|
|
|
|
auto g = MetadataGroup::Load(metadata_, group_index_);
|
|
#else
|
|
// The allocation size bounds the metadata array directly, so anchoring at
|
|
// the ends of the arrays lets the walk run off a single induction variable
|
|
// without ever dividing by the group size.
|
|
entries_end_ = view.entries_data() + alloc_size;
|
|
metadata_end_ = view.metadata() + alloc_size;
|
|
group_offset_ = -alloc_size;
|
|
|
|
auto g = MetadataGroup::Load(metadata_end_, group_offset_);
|
|
#endif
|
|
|
|
auto present_range = g.MatchPresent();
|
|
if (present_range) {
|
|
present_bits_ = static_cast<MatchBitsT>(present_range);
|
|
} else {
|
|
AdvanceToNextPresentGroup();
|
|
}
|
|
}
|
|
|
|
template <typename InputKeyT, typename InputValueT, typename InputKeyContextT>
|
|
[[clang::always_inline]] auto
|
|
ViewImpl<InputKeyT, InputValueT,
|
|
InputKeyContextT>::EntryRange::Iterator::AdvanceToNextPresentGroup()
|
|
-> void {
|
|
#ifndef NDEBUG
|
|
while (--groups_remaining_ >= 0) {
|
|
group_index_ = static_cast<ssize_t>(
|
|
static_cast<size_t>(group_index_ + step_) & probe_mask_);
|
|
auto g = MetadataGroup::Load(metadata_, group_index_);
|
|
auto range = g.MatchPresent();
|
|
if (range) {
|
|
group_entries_ = entries_ + group_index_;
|
|
present_bits_ = static_cast<MatchBitsT>(range);
|
|
return;
|
|
}
|
|
}
|
|
#else
|
|
for (group_offset_ += GroupSize; group_offset_ != 0;
|
|
group_offset_ += GroupSize) {
|
|
auto g = MetadataGroup::Load(metadata_end_, group_offset_);
|
|
auto range = g.MatchPresent();
|
|
if (range) {
|
|
present_bits_ = static_cast<MatchBitsT>(range);
|
|
return;
|
|
}
|
|
}
|
|
#endif
|
|
present_bits_ = 0;
|
|
}
|
|
|
|
template <typename InputKeyT, typename InputValueT, typename InputKeyContextT>
|
|
ViewImpl<InputKeyT, InputValueT, InputKeyContextT>::EntryRange::EntryRange(
|
|
ViewImpl view)
|
|
: view_(view) {
|
|
#ifndef NDEBUG
|
|
if (view_.alloc_size_ <= 0 || view_.storage_ == nullptr) {
|
|
return;
|
|
}
|
|
initial_metadata_hash_ = Carbon::HashValue(
|
|
llvm::ArrayRef<uint8_t>(view_.metadata(), view_.alloc_size_));
|
|
|
|
// Draw the traversal order once, here, so that `begin()` remains a pure
|
|
// function of the range and can be called repeatedly. Two separately
|
|
// constructed ranges still walk the table in different orders.
|
|
start_group_ = NextRangeEntropy().ExtractIndex() &
|
|
ComputeProbeMaskFromSize(view_.alloc_size_);
|
|
|
|
// Walk the groups with a stride of an odd number of groups. The group count
|
|
// is always a power of two, so any odd stride is coprime with it and visits
|
|
// every group exactly once before repeating. That scrambles the group order
|
|
// far more thoroughly than a forward or reverse scan, and costs nothing in
|
|
// the loop itself as the increment already adds a stride and masks.
|
|
ssize_t num_groups = view_.alloc_size_ / GroupSize;
|
|
ssize_t stride_groups =
|
|
(NextRangeEntropy().ExtractIndex() & (num_groups - 1)) | 1;
|
|
step_ = stride_groups * GroupSize;
|
|
#endif
|
|
}
|
|
|
|
#ifndef NDEBUG
|
|
template <typename InputKeyT, typename InputValueT, typename InputKeyContextT>
|
|
auto ViewImpl<InputKeyT, InputValueT,
|
|
InputKeyContextT>::EntryRange::CheckInvariants() const -> void {
|
|
if (view_.alloc_size_ <= 0 || view_.storage_ == nullptr) {
|
|
return;
|
|
}
|
|
HashCode current_hash = Carbon::HashValue(
|
|
llvm::ArrayRef<uint8_t>(view_.metadata(), view_.alloc_size_));
|
|
CARBON_CHECK(current_hash == initial_metadata_hash_,
|
|
"Hashtable mutated during iteration: metadata changed!");
|
|
}
|
|
#endif
|
|
|
|
template <typename InputKeyT, typename InputValueT, typename InputKeyContextT>
|
|
[[clang::always_inline]] auto
|
|
ViewImpl<InputKeyT, InputValueT, InputKeyContextT>::EntryRange::begin() const
|
|
-> Iterator {
|
|
// The traversal order is fixed when the range is constructed, so repeated
|
|
// calls yield equal iterators as forward ranges require.
|
|
return Iterator(*this);
|
|
}
|
|
|
|
template <typename InputKeyT, typename InputValueT, typename InputKeyContextT>
|
|
[[clang::always_inline]] auto
|
|
ViewImpl<InputKeyT, InputValueT, InputKeyContextT>::EntryRange::end() const
|
|
-> Iterator {
|
|
return Iterator();
|
|
}
|
|
|
|
// TODO: Evaluate whether it is worth forcing this out-of-line given the
|
|
// reasonable ABI boundary it forms and large volume of code necessary to
|
|
// implement it.
|
|
template <typename InputKeyT, typename InputValueT, typename InputKeyContextT>
|
|
template <typename LookupKeyT>
|
|
auto BaseImpl<InputKeyT, InputValueT, InputKeyContextT>::InsertImpl(
|
|
LookupKeyT lookup_key, KeyContextT key_context)
|
|
-> std::pair<EntryT*, bool> {
|
|
CARBON_DCHECK(alloc_size() > 0);
|
|
PrefetchStorage();
|
|
|
|
uint8_t* local_metadata = metadata();
|
|
|
|
HashCode hash = key_context.HashKey(lookup_key, ComputeSeed());
|
|
auto [hash_index, tag] = hash.ExtractIndexAndTag<7>();
|
|
|
|
// We re-purpose the empty control byte to signal no insert is needed to the
|
|
// caller. This is guaranteed to not be a control byte we're inserting.
|
|
// constexpr uint8_t NoInsertNeeded = Group::Empty;
|
|
|
|
ssize_t group_with_deleted_index;
|
|
MetadataGroup::MatchIndex deleted_match = {};
|
|
|
|
EntryT* local_entries = entries_data();
|
|
|
|
auto return_insert_at_index = [&](ssize_t index) -> std::pair<EntryT*, bool> {
|
|
// We'll need to insert at this index so set the control group byte to the
|
|
// proper value.
|
|
local_metadata[index] = tag | MetadataGroup::PresentMask;
|
|
return {&local_entries[index], true};
|
|
};
|
|
|
|
for (ProbeSequence s(hash_index, alloc_size());; s.Next()) {
|
|
ssize_t group_index = s.index();
|
|
|
|
// Load the group's metadata and prefetch the entries for this group. The
|
|
// prefetch here helps hide key access latency while we're matching the
|
|
// metadata.
|
|
auto g = MetadataGroup::Load(local_metadata, group_index);
|
|
EntryT* group_entries = &local_entries[group_index];
|
|
ViewImplT::PrefetchEntryGroup(group_entries);
|
|
|
|
auto control_byte_matched_range = g.Match(tag);
|
|
if (control_byte_matched_range) {
|
|
auto byte_it = control_byte_matched_range.begin();
|
|
auto byte_end = control_byte_matched_range.end();
|
|
do {
|
|
EntryT* entry = byte_it.index_ptr(group_entries);
|
|
if (LLVM_LIKELY(key_context.KeyEq(lookup_key, entry->key()))) {
|
|
return {entry, false};
|
|
}
|
|
++byte_it;
|
|
} while (LLVM_UNLIKELY(byte_it != byte_end));
|
|
}
|
|
|
|
// Track the first group with a deleted entry that we could insert over.
|
|
if (!deleted_match) {
|
|
deleted_match = g.MatchDeleted();
|
|
group_with_deleted_index = group_index;
|
|
}
|
|
|
|
// We failed to find a matching entry in this bucket, so check if there are
|
|
// no empty slots. In that case, we'll continue probing.
|
|
auto empty_match = g.MatchEmpty();
|
|
if (!empty_match) {
|
|
continue;
|
|
}
|
|
// Ok, we've finished probing without finding anything and need to insert
|
|
// instead.
|
|
|
|
// If we found a deleted slot, we don't need the probe sequence to insert
|
|
// so just bail. We want to ensure building up a table is fast so we
|
|
// de-prioritize this a bit. In practice this doesn't have too much of an
|
|
// effect.
|
|
if (LLVM_UNLIKELY(deleted_match)) {
|
|
return return_insert_at_index(group_with_deleted_index +
|
|
deleted_match.index());
|
|
}
|
|
|
|
// We're going to need to grow by inserting into an empty slot. Check that
|
|
// we have the budget for that before we compute the exact index of the
|
|
// empty slot. Without the growth budget we'll have to completely rehash and
|
|
// so we can just bail here.
|
|
if (LLVM_UNLIKELY(growth_budget_ == 0)) {
|
|
return {GrowAndInsert(hash, key_context), true};
|
|
}
|
|
|
|
--growth_budget_;
|
|
CARBON_DCHECK(growth_budget() >= 0,
|
|
"Growth budget shouldn't have gone negative!");
|
|
return return_insert_at_index(group_index + empty_match.index());
|
|
}
|
|
|
|
CARBON_FATAL(
|
|
"We should never finish probing without finding the entry or an empty "
|
|
"slot.");
|
|
}
|
|
|
|
template <typename InputKeyT, typename InputValueT, typename InputKeyContextT>
|
|
[[clang::noinline]] auto
|
|
BaseImpl<InputKeyT, InputValueT, InputKeyContextT>::GrowToAllocSizeImpl(
|
|
ssize_t target_alloc_size, KeyContextT key_context) -> void {
|
|
CARBON_CHECK(llvm::isPowerOf2_64(target_alloc_size));
|
|
if (target_alloc_size <= alloc_size()) {
|
|
return;
|
|
}
|
|
|
|
// If this is the next alloc size, we can used our optimized growth strategy.
|
|
if (target_alloc_size == ComputeNextAllocSize(alloc_size())) {
|
|
GrowToNextAllocSize(key_context);
|
|
return;
|
|
}
|
|
|
|
// Create locals for the old state of the table.
|
|
ssize_t old_size = alloc_size();
|
|
CARBON_DCHECK(old_size > 0);
|
|
bool old_small = is_small();
|
|
Storage* old_storage = storage();
|
|
uint8_t* old_metadata = metadata();
|
|
EntryT* old_entries = entries_data();
|
|
|
|
// Configure for the new size and allocate the new storage.
|
|
alloc_size() = target_alloc_size;
|
|
storage() = Allocate(target_alloc_size);
|
|
std::memset(metadata(), 0, target_alloc_size);
|
|
growth_budget_ = GrowthThresholdForAllocSize(target_alloc_size);
|
|
|
|
// Just re-insert all the entries. As we're more than doubling the table size,
|
|
// we don't bother with fancy optimizations here. Even using `memcpy` for the
|
|
// entries seems unlikely to be a significant win given how sparse the
|
|
// insertions will end up being.
|
|
ssize_t count = 0;
|
|
for (ssize_t group_index = 0; group_index < old_size;
|
|
group_index += GroupSize) {
|
|
auto g = MetadataGroup::Load(old_metadata, group_index);
|
|
auto present_matched_range = g.MatchPresent();
|
|
for (ssize_t byte_index : present_matched_range) {
|
|
++count;
|
|
ssize_t index = group_index + byte_index;
|
|
HashCode hash =
|
|
key_context.HashKey(old_entries[index].key(), ComputeSeed());
|
|
EntryT* new_entry = InsertIntoEmpty(hash);
|
|
new_entry->MoveFrom(std::move(old_entries[index]));
|
|
}
|
|
}
|
|
growth_budget_ -= count;
|
|
|
|
if (!old_small) {
|
|
// Old isn't a small buffer, so we need to deallocate it.
|
|
Deallocate(old_storage, old_size);
|
|
}
|
|
}
|
|
|
|
template <typename InputKeyT, typename InputValueT, typename InputKeyContextT>
|
|
auto BaseImpl<InputKeyT, InputValueT, InputKeyContextT>::GrowForInsertCountImpl(
|
|
ssize_t count, KeyContextT key_context) -> void {
|
|
if (count < growth_budget_) {
|
|
// Already space for the needed growth.
|
|
return;
|
|
}
|
|
|
|
// Currently, we don't account for any tombstones marking deleted elements,
|
|
// and just conservatively ensure the growth will create adequate growth
|
|
// budget for insertions. We could make this more precise by instead walking
|
|
// the table and only counting present slots, as once we grow we'll be able to
|
|
// reclaim all of the deleted slots. But this adds complexity and it isn't
|
|
// clear this is necessary so we do the simpler conservative thing.
|
|
ssize_t used_budget =
|
|
GrowthThresholdForAllocSize(alloc_size()) - growth_budget_;
|
|
ssize_t budget_needed = used_budget + count;
|
|
ssize_t space_needed = budget_needed + (budget_needed / 7);
|
|
ssize_t target_alloc_size = llvm::NextPowerOf2(space_needed);
|
|
CARBON_CHECK(GrowthThresholdForAllocSize(target_alloc_size) >
|
|
(budget_needed));
|
|
GrowToAllocSizeImpl(target_alloc_size, key_context);
|
|
}
|
|
|
|
template <typename InputKeyT, typename InputValueT, typename InputKeyContextT>
|
|
template <typename LookupKeyT>
|
|
auto BaseImpl<InputKeyT, InputValueT, InputKeyContextT>::EraseImpl(
|
|
LookupKeyT lookup_key, KeyContextT key_context) -> bool {
|
|
EntryT* entry = view_impl_.LookupEntry(lookup_key, key_context);
|
|
if (!entry) {
|
|
return false;
|
|
}
|
|
|
|
// If there are empty slots in this group then nothing will probe past this
|
|
// group looking for an entry so we can simply set this slot to empty as
|
|
// well. However, if every slot in this group is full, it might be part of
|
|
// a long probe chain that we can't disrupt. In that case we mark the slot's
|
|
// metadata as deleted to keep probes continuing past it.
|
|
//
|
|
// If we mark the slot as empty, we'll also need to increase the growth
|
|
// budget.
|
|
uint8_t* local_metadata = metadata();
|
|
EntryT* local_entries = entries_data();
|
|
ssize_t index = entry - local_entries;
|
|
ssize_t group_index = index & ~GroupMask;
|
|
auto g = MetadataGroup::Load(local_metadata, group_index);
|
|
auto empty_matched_range = g.MatchEmpty();
|
|
if (empty_matched_range) {
|
|
local_metadata[index] = MetadataGroup::Empty;
|
|
++growth_budget_;
|
|
} else {
|
|
local_metadata[index] = MetadataGroup::Deleted;
|
|
}
|
|
|
|
if constexpr (!EntryT::IsTriviallyDestructible) {
|
|
entry->Destroy();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
template <typename InputKeyT, typename InputValueT, typename InputKeyContextT>
|
|
auto BaseImpl<InputKeyT, InputValueT, InputKeyContextT>::ClearImpl() -> void {
|
|
DestroyEntries();
|
|
if (storage() != nullptr) {
|
|
std::memset(metadata(), 0, alloc_size());
|
|
}
|
|
growth_budget_ = GrowthThresholdForAllocSize(alloc_size());
|
|
}
|
|
|
|
// Allocates the appropriate memory layout for a table of the given
|
|
// `alloc_size`, with space both for the metadata array and entries.
|
|
//
|
|
// The returned pointer *must* be deallocated by calling the below `Deallocate`
|
|
// function with the same `alloc_size` as used here.
|
|
template <typename InputKeyT, typename InputValueT, typename InputKeyContextT>
|
|
auto BaseImpl<InputKeyT, InputValueT, InputKeyContextT>::Allocate(
|
|
ssize_t alloc_size) -> Storage* {
|
|
return reinterpret_cast<Storage*>(__builtin_operator_new(
|
|
ViewImplT::AllocByteSize(alloc_size),
|
|
static_cast<std::align_val_t>(Alignment), std::nothrow_t()));
|
|
}
|
|
|
|
// Deallocates a table's storage that was allocated with the `Allocate`
|
|
// function.
|
|
template <typename InputKeyT, typename InputValueT, typename InputKeyContextT>
|
|
auto BaseImpl<InputKeyT, InputValueT, InputKeyContextT>::Deallocate(
|
|
Storage* storage, ssize_t alloc_size) -> void {
|
|
ssize_t allocated_size = ViewImplT::AllocByteSize(alloc_size);
|
|
// We don't need the size, but make sure it always compiles.
|
|
static_cast<void>(allocated_size);
|
|
__builtin_operator_delete(storage,
|
|
#if __cpp_sized_deallocation
|
|
allocated_size,
|
|
#endif
|
|
static_cast<std::align_val_t>(Alignment));
|
|
}
|
|
|
|
// Construct a table using the provided small storage if `small_alloc_size_` is
|
|
// non-zero. If `small_alloc_size_` is zero, then `small_storage` won't be used
|
|
// and can be null. Regardless, after this the storage pointer is non-null and
|
|
// the size is non-zero so that we can directly begin inserting or querying the
|
|
// table.
|
|
template <typename InputKeyT, typename InputValueT, typename InputKeyContextT>
|
|
auto BaseImpl<InputKeyT, InputValueT, InputKeyContextT>::Construct(
|
|
Storage* small_storage) -> void {
|
|
if (small_alloc_size_ > 0) {
|
|
alloc_size() = small_alloc_size_;
|
|
storage() = small_storage;
|
|
} else {
|
|
// Directly allocate the initial buffer so that the hashtable is never in
|
|
// an empty state.
|
|
alloc_size() = MinAllocatedSize;
|
|
storage() = Allocate(MinAllocatedSize);
|
|
}
|
|
std::memset(metadata(), 0, alloc_size());
|
|
growth_budget_ = GrowthThresholdForAllocSize(alloc_size());
|
|
}
|
|
|
|
// Destroy the current table, releasing any memory used.
|
|
template <typename InputKeyT, typename InputValueT, typename InputKeyContextT>
|
|
auto BaseImpl<InputKeyT, InputValueT, InputKeyContextT>::Destroy() -> void {
|
|
// Check for a moved-from state and don't do anything. Only a moved-from table
|
|
// has a zero size.
|
|
if (alloc_size() == 0) {
|
|
return;
|
|
}
|
|
|
|
// Destroy all the entries.
|
|
DestroyEntries();
|
|
|
|
// If small, nothing to deallocate.
|
|
if (is_small()) {
|
|
return;
|
|
}
|
|
|
|
// Just deallocate the storage without updating anything when destroying the
|
|
// object.
|
|
Deallocate(storage(), alloc_size());
|
|
}
|
|
|
|
template <typename InputKeyT, typename InputValueT, typename InputKeyContextT>
|
|
auto BaseImpl<InputKeyT, InputValueT, InputKeyContextT>::DestroyEntries()
|
|
-> void {
|
|
if constexpr (!EntryT::IsTriviallyDestructible) {
|
|
for (typename EntryT::RefT entry : view_impl_.EntriesImpl()) {
|
|
EntryT::DestroyRef(entry);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Copy all of the slots over from another table that is exactly the same
|
|
// allocation size.
|
|
//
|
|
// This requires the current table to already have storage allocated and set up
|
|
// but not initialized (or already cleared). It directly overwrites the storage
|
|
// allocation of the table to match the incoming argument.
|
|
//
|
|
// Despite being used in construction, this shouldn't be called for a moved-from
|
|
// `arg` -- in practice it is better for callers to handle this when setting up
|
|
// storage.
|
|
template <typename InputKeyT, typename InputValueT, typename InputKeyContextT>
|
|
auto BaseImpl<InputKeyT, InputValueT, InputKeyContextT>::CopySlotsFrom(
|
|
const BaseImpl& arg) -> void
|
|
requires(EntryT::IsCopyable)
|
|
{
|
|
CARBON_DCHECK(alloc_size() == arg.alloc_size());
|
|
ssize_t local_size = alloc_size();
|
|
|
|
// Preserve which slot every entry is in, including tombstones in the
|
|
// metadata, in order to copy into the new table's storage without rehashing
|
|
// all of the keys. This is especially important as we don't have an easy way
|
|
// to access the key context needed for rehashing here.
|
|
uint8_t* local_metadata = metadata();
|
|
EntryT* local_entries = entries_data();
|
|
const uint8_t* local_arg_metadata = arg.metadata();
|
|
const EntryT* local_arg_entries = arg.entries_data();
|
|
memcpy(local_metadata, local_arg_metadata, local_size);
|
|
|
|
for (ssize_t group_index = 0; group_index < local_size;
|
|
group_index += GroupSize) {
|
|
auto g = MetadataGroup::Load(local_arg_metadata, group_index);
|
|
for (ssize_t byte_index : g.MatchPresent()) {
|
|
local_entries[group_index + byte_index].CopyFrom(
|
|
local_arg_entries[group_index + byte_index]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Move from another table to this one.
|
|
//
|
|
// Note that the `small_storage` is *this* table's small storage pointer,
|
|
// provided from the `TableImpl` to this `BaseImpl` method as an argument.
|
|
//
|
|
// Requires the table to have size and growth already set up but otherwise the
|
|
// the table has not yet been initialized. Notably, storage should either not
|
|
// yet be constructed or already destroyed. It both sets up the storage and
|
|
// handles any moving slots needed.
|
|
//
|
|
// Note that because this is used in construction it needs to handle a
|
|
// moved-from `arg`.
|
|
template <typename InputKeyT, typename InputValueT, typename InputKeyContextT>
|
|
auto BaseImpl<InputKeyT, InputValueT, InputKeyContextT>::MoveFrom(
|
|
BaseImpl&& arg, Storage* small_storage) -> void {
|
|
ssize_t local_size = alloc_size();
|
|
CARBON_DCHECK(local_size == arg.alloc_size());
|
|
// If `arg` is moved-from, skip the rest as the local size is all we need.
|
|
if (local_size == 0) {
|
|
return;
|
|
}
|
|
|
|
if (arg.is_small()) {
|
|
CARBON_DCHECK(local_size == small_alloc_size_);
|
|
this->storage() = small_storage;
|
|
|
|
// For small tables, we have to move the entries as we can't move the tables
|
|
// themselves. We do this preserving their slots and even tombstones to
|
|
// avoid rehashing.
|
|
uint8_t* local_metadata = this->metadata();
|
|
EntryT* local_entries = this->entries_data();
|
|
uint8_t* local_arg_metadata = arg.metadata();
|
|
EntryT* local_arg_entries = arg.entries_data();
|
|
memcpy(local_metadata, local_arg_metadata, local_size);
|
|
if (EntryT::IsTriviallyRelocatable) {
|
|
memcpy(local_entries, local_arg_entries, local_size * sizeof(EntryT));
|
|
} else {
|
|
for (ssize_t group_index = 0; group_index < local_size;
|
|
group_index += GroupSize) {
|
|
auto g = MetadataGroup::Load(local_arg_metadata, group_index);
|
|
for (ssize_t byte_index : g.MatchPresent()) {
|
|
local_entries[group_index + byte_index].MoveFrom(
|
|
std::move(local_arg_entries[group_index + byte_index]));
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Just point to the allocated storage.
|
|
storage() = arg.storage();
|
|
}
|
|
|
|
// Finally, put the incoming table into a moved-from state.
|
|
arg.alloc_size() = 0;
|
|
// Replace the pointer with null to ease debugging.
|
|
arg.storage() = nullptr;
|
|
}
|
|
|
|
// Optimized routine to insert a key into a table when that key *definitely*
|
|
// isn't present in the table and the table *definitely* has a viable empty slot
|
|
// (and growth space) to insert into before any deleted slots. When both of
|
|
// these are true, typically just after growth, we can dramatically simplify the
|
|
// insert position search.
|
|
template <typename InputKeyT, typename InputValueT, typename InputKeyContextT>
|
|
auto BaseImpl<InputKeyT, InputValueT, InputKeyContextT>::InsertIntoEmpty(
|
|
HashCode hash) -> EntryT* {
|
|
auto [hash_index, tag] = hash.ExtractIndexAndTag<7>();
|
|
uint8_t* local_metadata = metadata();
|
|
EntryT* local_entries = entries_data();
|
|
|
|
for (ProbeSequence s(hash_index, alloc_size());; s.Next()) {
|
|
ssize_t group_index = s.index();
|
|
auto g = MetadataGroup::Load(local_metadata, group_index);
|
|
|
|
if (auto empty_match = g.MatchEmpty()) {
|
|
ssize_t index = group_index + empty_match.index();
|
|
local_metadata[index] = tag | MetadataGroup::PresentMask;
|
|
return &local_entries[index];
|
|
}
|
|
|
|
// Otherwise we continue probing.
|
|
}
|
|
}
|
|
|
|
// Apply our doubling growth strategy and (re-)check invariants around table
|
|
// size.
|
|
template <typename InputKeyT, typename InputValueT, typename InputKeyContextT>
|
|
auto BaseImpl<InputKeyT, InputValueT, InputKeyContextT>::ComputeNextAllocSize(
|
|
ssize_t old_alloc_size) -> ssize_t {
|
|
CARBON_DCHECK(llvm::isPowerOf2_64(old_alloc_size),
|
|
"Expected a power of two!");
|
|
ssize_t new_alloc_size;
|
|
bool overflow = __builtin_mul_overflow(old_alloc_size, 2, &new_alloc_size);
|
|
CARBON_CHECK(!overflow, "Computing the new size overflowed `ssize_t`!");
|
|
return new_alloc_size;
|
|
}
|
|
|
|
// Compute the growth threshold for a given size.
|
|
template <typename InputKeyT, typename InputValueT, typename InputKeyContextT>
|
|
auto BaseImpl<InputKeyT, InputValueT,
|
|
InputKeyContextT>::GrowthThresholdForAllocSize(ssize_t alloc_size)
|
|
-> ssize_t {
|
|
// We use a 7/8ths load factor to trigger growth.
|
|
return alloc_size - alloc_size / 8;
|
|
}
|
|
|
|
// Optimized routine for growing to the next alloc size.
|
|
//
|
|
// A particularly common and important-to-optimize path is growing to the next
|
|
// alloc size, which will always be a doubling of the allocated size. This
|
|
// allows an important optimization -- we're adding exactly one more high bit to
|
|
// the hash-computed index for each entry. This in turn means we can classify
|
|
// every entry in the table into three cases:
|
|
//
|
|
// 1) The new high bit is zero, the entry is at the same index in the new
|
|
// table as the old.
|
|
//
|
|
// 2) The new high bit is one, the entry is at the old index plus the old
|
|
// size.
|
|
//
|
|
// 3) The entry's current index doesn't match the initial hash index because
|
|
// it required some amount of probing to find an empty slot.
|
|
//
|
|
// The design of the hash table tries to minimize how many entries fall into
|
|
// case (3), so we expect the vast majority of entries to be in (1) or (2). This
|
|
// lets us model growth notionally as copying the hashtable twice into the lower
|
|
// and higher halves of the new allocation, clearing out the now-empty slots
|
|
// (from both deleted entries and entries in the other half of the table after
|
|
// growth), and inserting any probed elements. That model in turn is much more
|
|
// efficient than re-inserting all of the elements as it avoids the unnecessary
|
|
// parts of insertion and avoids interleaving random accesses for the probed
|
|
// elements. But most importantly, for trivially relocatable types it allows us
|
|
// to use `memcpy` rather than moving the elements individually.
|
|
template <typename InputKeyT, typename InputValueT, typename InputKeyContextT>
|
|
auto BaseImpl<InputKeyT, InputValueT, InputKeyContextT>::GrowToNextAllocSize(
|
|
KeyContextT key_context) -> void {
|
|
// We collect the probed elements in a small vector for re-insertion. It is
|
|
// tempting to reuse the already allocated storage, but doing so appears to
|
|
// be a (very slight) performance regression. These are relatively rare and
|
|
// storing them into the existing storage creates stores to the same regions
|
|
// of memory we're reading. Moreover, it requires moving both the key and the
|
|
// value twice, and doing the `memcpy` widening for relocatable types before
|
|
// the group walk rather than after the group walk. In practice, between the
|
|
// statistical rareness and using a large small size buffer here on the stack,
|
|
// we can handle this most efficiently with temporary, additional storage.
|
|
llvm::SmallVector<std::pair<ssize_t, HashCode>, 128> probed_indices;
|
|
|
|
// Create locals for the old state of the table.
|
|
ssize_t old_size = alloc_size();
|
|
CARBON_DCHECK(old_size > 0);
|
|
|
|
bool old_small = is_small();
|
|
Storage* old_storage = storage();
|
|
uint8_t* old_metadata = metadata();
|
|
EntryT* old_entries = entries_data();
|
|
|
|
#ifndef NDEBUG
|
|
// Count how many of the old table slots will end up being empty after we grow
|
|
// the table. This is both the currently empty slots, but also the deleted
|
|
// slots because we clear them to empty and re-insert everything that had any
|
|
// probing.
|
|
ssize_t debug_empty_count =
|
|
llvm::count(llvm::ArrayRef(old_metadata, old_size), MetadataGroup::Empty);
|
|
ssize_t debug_deleted_count = llvm::count(
|
|
llvm::ArrayRef(old_metadata, old_size), MetadataGroup::Deleted);
|
|
CARBON_DCHECK(
|
|
debug_empty_count >= (old_size - GrowthThresholdForAllocSize(old_size)),
|
|
"debug_empty_count: {0}, debug_deleted_count: {1}, size: {2}",
|
|
debug_empty_count, debug_deleted_count, old_size);
|
|
#endif
|
|
|
|
// Configure for the new size and allocate the new storage.
|
|
ssize_t new_size = ComputeNextAllocSize(old_size);
|
|
alloc_size() = new_size;
|
|
storage() = Allocate(new_size);
|
|
growth_budget_ = GrowthThresholdForAllocSize(new_size);
|
|
|
|
// Now extract the new components of the table.
|
|
uint8_t* new_metadata = metadata();
|
|
EntryT* new_entries = entries_data();
|
|
|
|
// Walk the metadata groups, clearing deleted to empty, duplicating the
|
|
// metadata for the low and high halves, and updating it based on where each
|
|
// entry will go in the new table. The updated metadata group is written to
|
|
// the new table, and for non-trivially relocatable entry types, the entry is
|
|
// also moved to its new location.
|
|
ssize_t count = 0;
|
|
for (ssize_t group_index = 0; group_index < old_size;
|
|
group_index += GroupSize) {
|
|
auto low_g = MetadataGroup::Load(old_metadata, group_index);
|
|
// Make sure to match present elements first to enable pipelining with
|
|
// clearing.
|
|
auto present_matched_range = low_g.MatchPresent();
|
|
low_g.ClearDeleted();
|
|
MetadataGroup high_g;
|
|
if constexpr (MetadataGroup::FastByteClear) {
|
|
// When we have a fast byte clear, we can update the metadata for the
|
|
// growth in-register and store at the end.
|
|
high_g = low_g;
|
|
} else {
|
|
// If we don't have a fast byte clear, we can store the metadata group
|
|
// eagerly here and overwrite bytes with a byte store below instead of
|
|
// clearing the byte in-register.
|
|
low_g.Store(new_metadata, group_index);
|
|
low_g.Store(new_metadata, group_index | old_size);
|
|
}
|
|
for (ssize_t byte_index : present_matched_range) {
|
|
++count;
|
|
ssize_t old_index = group_index + byte_index;
|
|
if constexpr (!MetadataGroup::FastByteClear) {
|
|
CARBON_DCHECK(new_metadata[old_index] == old_metadata[old_index]);
|
|
CARBON_DCHECK(new_metadata[old_index | old_size] ==
|
|
old_metadata[old_index]);
|
|
}
|
|
HashCode hash =
|
|
key_context.HashKey(old_entries[old_index].key(), ComputeSeed());
|
|
ssize_t old_hash_index = hash.ExtractIndexAndTag<7>().first &
|
|
ComputeProbeMaskFromSize(old_size);
|
|
if (LLVM_UNLIKELY(old_hash_index != group_index)) {
|
|
probed_indices.push_back({old_index, hash});
|
|
if constexpr (MetadataGroup::FastByteClear) {
|
|
low_g.ClearByte(byte_index);
|
|
high_g.ClearByte(byte_index);
|
|
} else {
|
|
new_metadata[old_index] = MetadataGroup::Empty;
|
|
new_metadata[old_index | old_size] = MetadataGroup::Empty;
|
|
}
|
|
continue;
|
|
}
|
|
ssize_t new_index = hash.ExtractIndexAndTag<7>().first &
|
|
ComputeProbeMaskFromSize(new_size);
|
|
CARBON_DCHECK(new_index == old_hash_index ||
|
|
new_index == (old_hash_index | old_size));
|
|
// Toggle the newly added bit of the index to get to the other possible
|
|
// target index.
|
|
if constexpr (MetadataGroup::FastByteClear) {
|
|
(new_index == old_hash_index ? high_g : low_g).ClearByte(byte_index);
|
|
new_index += byte_index;
|
|
} else {
|
|
new_index += byte_index;
|
|
new_metadata[new_index ^ old_size] = MetadataGroup::Empty;
|
|
}
|
|
|
|
// If we need to explicitly move (and destroy) the key or value, do so
|
|
// here where we already know its target.
|
|
if constexpr (!EntryT::IsTriviallyRelocatable) {
|
|
new_entries[new_index].MoveFrom(std::move(old_entries[old_index]));
|
|
}
|
|
}
|
|
if constexpr (MetadataGroup::FastByteClear) {
|
|
low_g.Store(new_metadata, group_index);
|
|
high_g.Store(new_metadata, (group_index | old_size));
|
|
}
|
|
}
|
|
CARBON_DCHECK((count - static_cast<ssize_t>(probed_indices.size())) ==
|
|
(new_size - llvm::count(llvm::ArrayRef(new_metadata, new_size),
|
|
MetadataGroup::Empty)));
|
|
#ifndef NDEBUG
|
|
CARBON_DCHECK((debug_empty_count + debug_deleted_count) ==
|
|
(old_size - count));
|
|
CARBON_DCHECK(llvm::count(llvm::ArrayRef(new_metadata, new_size),
|
|
MetadataGroup::Empty) ==
|
|
debug_empty_count + debug_deleted_count +
|
|
static_cast<ssize_t>(probed_indices.size()) + old_size);
|
|
#endif
|
|
|
|
// If the keys or values are trivially relocatable, we do a bulk memcpy of
|
|
// them into place. This will copy them into both possible locations, which is
|
|
// fine. One will be empty and clobbered if reused or ignored. The other will
|
|
// be the one used. This might seem like it needs it to be valid for us to
|
|
// create two copies, but it doesn't. This produces the exact same storage as
|
|
// copying the storage into the wrong location first, and then again into the
|
|
// correct location. Only one is live and only one is destroyed.
|
|
if constexpr (EntryT::IsTriviallyRelocatable) {
|
|
memcpy(new_entries, old_entries, old_size * sizeof(EntryT));
|
|
memcpy(new_entries + old_size, old_entries, old_size * sizeof(EntryT));
|
|
}
|
|
|
|
// We then need to do a normal insertion for anything that was probed before
|
|
// growth, but we know we'll find an empty slot, so leverage that.
|
|
for (auto [old_index, hash] : probed_indices) {
|
|
EntryT* new_entry = InsertIntoEmpty(hash);
|
|
new_entry->MoveFrom(std::move(old_entries[old_index]));
|
|
}
|
|
CARBON_DCHECK(count ==
|
|
(new_size - llvm::count(llvm::ArrayRef(new_metadata, new_size),
|
|
MetadataGroup::Empty)));
|
|
growth_budget_ -= count;
|
|
CARBON_DCHECK(growth_budget_ ==
|
|
(GrowthThresholdForAllocSize(new_size) -
|
|
(new_size - llvm::count(llvm::ArrayRef(new_metadata, new_size),
|
|
MetadataGroup::Empty))));
|
|
CARBON_DCHECK(growth_budget_ > 0 &&
|
|
"Must still have a growth budget after rehash!");
|
|
|
|
if (!old_small) {
|
|
// Old isn't a small buffer, so we need to deallocate it.
|
|
Deallocate(old_storage, old_size);
|
|
}
|
|
}
|
|
|
|
// Grow the hashtable to create space and then insert into it. Returns the
|
|
// selected insertion entry. Never returns null. In addition to growing and
|
|
// selecting the insertion entry, this routine updates the metadata array so
|
|
// that this function can be directly called and the result returned from
|
|
// `InsertImpl`.
|
|
template <typename InputKeyT, typename InputValueT, typename InputKeyContextT>
|
|
[[clang::noinline]] auto
|
|
BaseImpl<InputKeyT, InputValueT, InputKeyContextT>::GrowAndInsert(
|
|
HashCode hash, KeyContextT key_context) -> EntryT* {
|
|
GrowToNextAllocSize(key_context);
|
|
|
|
// And insert the lookup_key into an index in the newly grown map and return
|
|
// that index for use.
|
|
--growth_budget_;
|
|
return InsertIntoEmpty(hash);
|
|
}
|
|
|
|
template <typename InputBaseT, ssize_t SmallSize>
|
|
TableImpl<InputBaseT, SmallSize>::TableImpl(const TableImpl& arg)
|
|
requires(BaseT::EntryT::IsCopyable)
|
|
: BaseT(arg.alloc_size(), arg.growth_budget_, SmallSize) {
|
|
// Check for completely broken objects. These invariants should be true even
|
|
// in a moved-from state.
|
|
CARBON_DCHECK(arg.alloc_size() == 0 || !arg.is_small() ||
|
|
arg.alloc_size() == SmallSize);
|
|
CARBON_DCHECK(arg.small_alloc_size_ == SmallSize);
|
|
CARBON_DCHECK(this->small_alloc_size_ == SmallSize);
|
|
|
|
if (this->alloc_size() != 0) {
|
|
SetUpStorage();
|
|
this->CopySlotsFrom(arg);
|
|
}
|
|
}
|
|
|
|
template <typename InputBaseT, ssize_t SmallSize>
|
|
auto TableImpl<InputBaseT, SmallSize>::operator=(const TableImpl& arg)
|
|
-> TableImpl&
|
|
requires(BaseT::EntryT::IsCopyable)
|
|
{
|
|
// Check for completely broken objects. These invariants should be true even
|
|
// in a moved-from state.
|
|
CARBON_DCHECK(arg.alloc_size() == 0 || !arg.is_small() ||
|
|
arg.alloc_size() == SmallSize);
|
|
CARBON_DCHECK(arg.small_alloc_size_ == SmallSize);
|
|
CARBON_DCHECK(this->small_alloc_size_ == SmallSize);
|
|
|
|
// We have to end up with an allocation size exactly equivalent to the
|
|
// incoming argument to avoid re-hashing every entry in the table, which isn't
|
|
// possible without key context.
|
|
if (arg.alloc_size() == this->alloc_size()) {
|
|
// No effective way for self-assignment to fall out of an efficient
|
|
// implementation so detect and bypass here. Similarly, if both are in a
|
|
// moved-from state, there is nothing to do.
|
|
if (&arg == this || this->alloc_size() == 0) {
|
|
return *this;
|
|
}
|
|
CARBON_DCHECK(arg.storage() != this->storage());
|
|
this->DestroyEntries();
|
|
} else {
|
|
// The sizes don't match so destroy everything and re-setup the table
|
|
// storage.
|
|
this->Destroy();
|
|
this->alloc_size() = arg.alloc_size();
|
|
// If `arg` is moved-from, we've clear out our elements and put ourselves
|
|
// into a moved-from state. We're done.
|
|
if (this->alloc_size() == 0) {
|
|
return *this;
|
|
}
|
|
SetUpStorage();
|
|
}
|
|
this->growth_budget_ = arg.growth_budget_;
|
|
this->CopySlotsFrom(arg);
|
|
return *this;
|
|
}
|
|
|
|
// Puts the incoming table into a moved-from state that can be destroyed or
|
|
// re-initialized but must not be used otherwise.
|
|
template <typename InputBaseT, ssize_t SmallSize>
|
|
TableImpl<InputBaseT, SmallSize>::TableImpl(TableImpl&& arg) noexcept
|
|
: BaseT(arg.alloc_size(), arg.growth_budget_, SmallSize) {
|
|
// Check for completely broken objects. These invariants should be true even
|
|
// in a moved-from state.
|
|
CARBON_DCHECK(arg.alloc_size() == 0 || !arg.is_small() ||
|
|
arg.alloc_size() == SmallSize);
|
|
CARBON_DCHECK(arg.small_alloc_size_ == SmallSize);
|
|
CARBON_DCHECK(this->small_alloc_size_ == SmallSize);
|
|
this->MoveFrom(std::move(arg), small_storage());
|
|
}
|
|
|
|
template <typename InputBaseT, ssize_t SmallSize>
|
|
auto TableImpl<InputBaseT, SmallSize>::operator=(TableImpl&& arg) noexcept
|
|
-> TableImpl& {
|
|
// Check for completely broken objects. These invariants should be true even
|
|
// in a moved-from state.
|
|
CARBON_DCHECK(arg.alloc_size() == 0 || !arg.is_small() ||
|
|
arg.alloc_size() == SmallSize);
|
|
CARBON_DCHECK(arg.small_alloc_size_ == SmallSize);
|
|
CARBON_DCHECK(this->small_alloc_size_ == SmallSize);
|
|
|
|
// Destroy and deallocate our table.
|
|
this->Destroy();
|
|
|
|
// Defend against self-move by zeroing the size here before we start moving
|
|
// out of `arg`.
|
|
this->alloc_size() = 0;
|
|
|
|
// Setup to match argument and then finish the move.
|
|
this->alloc_size() = arg.alloc_size();
|
|
this->growth_budget_ = arg.growth_budget_;
|
|
this->MoveFrom(std::move(arg), small_storage());
|
|
return *this;
|
|
}
|
|
|
|
template <typename InputBaseT, ssize_t SmallSize>
|
|
TableImpl<InputBaseT, SmallSize>::~TableImpl() {
|
|
this->Destroy();
|
|
}
|
|
|
|
// Reset a table to its original state, including releasing any allocated
|
|
// memory.
|
|
template <typename InputBaseT, ssize_t SmallSize>
|
|
auto TableImpl<InputBaseT, SmallSize>::ResetImpl() -> void {
|
|
this->Destroy();
|
|
|
|
// Re-initialize the whole thing.
|
|
CARBON_DCHECK(this->small_alloc_size() == SmallSize);
|
|
this->Construct(small_storage());
|
|
}
|
|
|
|
template <typename InputBaseT, ssize_t SmallSize>
|
|
auto TableImpl<InputBaseT, SmallSize>::small_storage() const -> Storage* {
|
|
if constexpr (SmallSize > 0) {
|
|
// Do a bunch of validation of the small size to establish our invariants
|
|
// when we know we have a non-zero small size.
|
|
static_assert(llvm::isPowerOf2_64(SmallSize),
|
|
"SmallSize must be a power of two for a hashed buffer!");
|
|
static_assert(
|
|
SmallSize >= MaxGroupSize,
|
|
"We require all small sizes to multiples of the largest group "
|
|
"size supported to ensure it can be used portably. ");
|
|
static_assert(
|
|
(SmallSize % MaxGroupSize) == 0,
|
|
"Small size must be a multiple of the max group size supported "
|
|
"so that we can allocate a whole number of groups.");
|
|
// Implied by the max asserts above.
|
|
static_assert(SmallSize >= GroupSize);
|
|
static_assert((SmallSize % GroupSize) == 0);
|
|
|
|
static_assert(SmallSize >= alignof(StorageEntry<KeyT, ValueT>),
|
|
"Requested a small size that would require padding between "
|
|
"metadata bytes and correctly aligned key and value types. "
|
|
"Either a larger small size or a zero small size and heap "
|
|
"allocation are required for this key and value type.");
|
|
|
|
static_assert(offsetof(SmallStorage, entries) == SmallSize,
|
|
"Offset to entries in small size storage doesn't match "
|
|
"computed offset!");
|
|
|
|
return &small_storage_;
|
|
} else {
|
|
static_assert(
|
|
sizeof(TableImpl) == sizeof(BaseT),
|
|
"Empty small storage caused a size difference and wasted space!");
|
|
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
// Helper to set up the storage of a table when a specific size has already been
|
|
// set up. If possible, uses any small storage, otherwise allocates.
|
|
template <typename InputBaseT, ssize_t SmallSize>
|
|
auto TableImpl<InputBaseT, SmallSize>::SetUpStorage() -> void {
|
|
CARBON_DCHECK(this->small_alloc_size() == SmallSize);
|
|
ssize_t local_size = this->alloc_size();
|
|
CARBON_DCHECK(local_size != 0);
|
|
if (local_size == SmallSize) {
|
|
this->storage() = small_storage();
|
|
} else {
|
|
this->storage() = BaseT::Allocate(local_size);
|
|
}
|
|
}
|
|
|
|
} // namespace Carbon::RawHashtable
|
|
|
|
#endif // CARBON_COMMON_RAW_HASHTABLE_H_
|