Skip to content

Commit 5be22f9

Browse files
ezbrcopybara-github
authored andcommitted
Move growth_left to the backing array.
PiperOrigin-RevId: 548794485 Change-Id: Ie82d5f8ad752518ef05b38144ca1e32b21c9def8
1 parent be85b34 commit 5be22f9

File tree

3 files changed

+125
-49
lines changed

3 files changed

+125
-49
lines changed

absl/container/internal/raw_hash_set.cc

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include <cstddef>
2020
#include <cstring>
2121

22+
#include "absl/base/attributes.h"
2223
#include "absl/base/config.h"
2324
#include "absl/base/dynamic_annotations.h"
2425
#include "absl/hash/hash.h"
@@ -27,9 +28,17 @@ namespace absl {
2728
ABSL_NAMESPACE_BEGIN
2829
namespace container_internal {
2930

30-
// A single block of empty control bytes for tables without any slots allocated.
31-
// This enables removing a branch in the hot path of find().
32-
alignas(16) ABSL_CONST_INIT ABSL_DLL const ctrl_t kEmptyGroup[16] = {
31+
// We have space for `growth_left` before a single block of control bytes. A
32+
// single block of empty control bytes for tables without any slots allocated.
33+
// This enables removing a branch in the hot path of find(). In order to ensure
34+
// that the control bytes are aligned to 16, we have 16 bytes before the control
35+
// bytes even though growth_left only needs 8.
36+
constexpr ctrl_t ZeroCtrlT() { return static_cast<ctrl_t>(0); }
37+
alignas(16) ABSL_CONST_INIT ABSL_DLL const ctrl_t kEmptyGroup[32] = {
38+
ZeroCtrlT(), ZeroCtrlT(), ZeroCtrlT(), ZeroCtrlT(),
39+
ZeroCtrlT(), ZeroCtrlT(), ZeroCtrlT(), ZeroCtrlT(),
40+
ZeroCtrlT(), ZeroCtrlT(), ZeroCtrlT(), ZeroCtrlT(),
41+
ZeroCtrlT(), ZeroCtrlT(), ZeroCtrlT(), ZeroCtrlT(),
3342
ctrl_t::kSentinel, ctrl_t::kEmpty, ctrl_t::kEmpty, ctrl_t::kEmpty,
3443
ctrl_t::kEmpty, ctrl_t::kEmpty, ctrl_t::kEmpty, ctrl_t::kEmpty,
3544
ctrl_t::kEmpty, ctrl_t::kEmpty, ctrl_t::kEmpty, ctrl_t::kEmpty,
@@ -239,12 +248,12 @@ void ClearBackingArray(CommonFields& c, const PolicyFunctions& policy,
239248
c.infoz().RecordStorageChanged(0, c.capacity());
240249
} else {
241250
void* set = &c;
242-
(*policy.dealloc)(set, policy, c.control(), c.slots_ptr(), c.capacity());
251+
(*policy.dealloc)(set, policy, c.backing_array_start(), c.slots_ptr(),
252+
c.capacity());
243253
c.set_control(EmptyGroup());
244254
c.set_generation_ptr(EmptyGeneration());
245255
c.set_slots(nullptr);
246256
c.set_capacity(0);
247-
c.set_growth_left(0);
248257
c.infoz().RecordClearedReservation();
249258
assert(c.size() == 0);
250259
c.infoz().RecordStorageChanged(0, 0);

absl/container/internal/raw_hash_set.h

Lines changed: 67 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@
6262
// pseudo-struct:
6363
//
6464
// struct BackingArray {
65+
// // The number of elements we can insert before growing the capacity.
66+
// size_t growth_left;
6567
// // Control bytes for the "real" slots.
6668
// ctrl_t ctrl[capacity];
6769
// // Always `ctrl_t::kSentinel`. This is used by iterators to find when to
@@ -174,6 +176,7 @@
174176

175177
#include <algorithm>
176178
#include <cmath>
179+
#include <cstddef>
177180
#include <cstdint>
178181
#include <cstring>
179182
#include <iterator>
@@ -484,13 +487,14 @@ static_assert(ctrl_t::kDeleted == static_cast<ctrl_t>(-2),
484487
"ctrl_t::kDeleted must be -2 to make the implementation of "
485488
"ConvertSpecialToEmptyAndFullToDeleted efficient");
486489

487-
ABSL_DLL extern const ctrl_t kEmptyGroup[16];
490+
// See definition comment for why this is size 32.
491+
ABSL_DLL extern const ctrl_t kEmptyGroup[32];
488492

489493
// Returns a pointer to a control byte group that can be used by empty tables.
490494
inline ctrl_t* EmptyGroup() {
491495
// Const must be cast away here; no uses of this function will actually write
492496
// to it, because it is only used for empty tables.
493-
return const_cast<ctrl_t*>(kEmptyGroup);
497+
return const_cast<ctrl_t*>(kEmptyGroup + 16);
494498
}
495499

496500
// Returns a pointer to a generation to use for an empty hashtable.
@@ -913,33 +917,46 @@ class CommonFields : public CommonFieldsGenerationInfo {
913917
// fields generates less code then calling absl::exchange per field.
914918
control_(that.control()),
915919
slots_(that.slots_ptr()),
916-
size_(that.size()),
917920
capacity_(that.capacity()),
918-
compressed_tuple_(that.growth_left(), std::move(that.infoz())) {
921+
compressed_tuple_(that.size(), std::move(that.infoz())) {
919922
that.set_control(EmptyGroup());
920923
that.set_slots(nullptr);
921-
that.set_size(0);
922924
that.set_capacity(0);
923-
that.set_growth_left(0);
925+
that.set_size(0);
924926
}
925927
CommonFields& operator=(CommonFields&&) = default;
926928

927929
ctrl_t* control() const { return control_; }
928930
void set_control(ctrl_t* c) { control_ = c; }
931+
void* backing_array_start() const {
932+
// growth_left is stored before control bytes.
933+
assert(reinterpret_cast<uintptr_t>(control()) % alignof(size_t) == 0);
934+
return control() - sizeof(size_t);
935+
}
936+
929937
// Note: we can't use slots() because Qt defines "slots" as a macro.
930938
void* slots_ptr() const { return slots_; }
931939
void set_slots(void* s) { slots_ = s; }
932-
size_t size() const { return size_; }
933-
void set_size(size_t s) { size_ = s; }
940+
941+
// The number of filled slots.
942+
size_t size() const { return compressed_tuple_.template get<0>(); }
943+
void set_size(size_t s) { compressed_tuple_.template get<0>() = s; }
944+
945+
// The total number of available slots.
934946
size_t capacity() const { return capacity_; }
935947
void set_capacity(size_t c) {
936948
assert(c == 0 || IsValidCapacity(c));
937949
capacity_ = c;
938950
}
939951

940952
// The number of slots we can still fill without needing to rehash.
941-
size_t growth_left() const { return compressed_tuple_.template get<0>(); }
942-
void set_growth_left(size_t gl) { compressed_tuple_.template get<0>() = gl; }
953+
// This is stored in the heap allocation before the control bytes.
954+
size_t growth_left() const {
955+
return *reinterpret_cast<size_t*>(backing_array_start());
956+
}
957+
void set_growth_left(size_t gl) {
958+
*reinterpret_cast<size_t*>(backing_array_start()) = gl;
959+
}
943960

944961
HashtablezInfoHandle& infoz() { return compressed_tuple_.template get<1>(); }
945962
const HashtablezInfoHandle& infoz() const {
@@ -951,32 +968,30 @@ class CommonFields : public CommonFieldsGenerationInfo {
951968
should_rehash_for_bug_detection_on_insert(control(), capacity());
952969
}
953970
void reset_reserved_growth(size_t reservation) {
954-
CommonFieldsGenerationInfo::reset_reserved_growth(reservation, size_);
971+
CommonFieldsGenerationInfo::reset_reserved_growth(reservation, size());
955972
}
956973

957974
private:
958975
// TODO(b/259599413): Investigate removing some of these fields:
959976
// - control/slots can be derived from each other
960-
// - growth_left can be moved into the slot array
961977
// - we can use 6 bits for capacity since it's always a power of two minus 1
962978

963-
// The control bytes (and, also, a pointer to the base of the backing array).
979+
// The control bytes (and, also, a pointer near to the base of the backing
980+
// array).
964981
//
965982
// This contains `capacity + 1 + NumClonedBytes()` entries, even
966983
// when the table is empty (hence EmptyGroup).
984+
//
985+
// Note that growth_left is stored immediately before this pointer.
967986
ctrl_t* control_ = EmptyGroup();
968987

969988
// The beginning of the slots, located at `SlotOffset()` bytes after
970989
// `control`. May be null for empty tables.
971990
void* slots_ = nullptr;
972991

973-
// The number of filled slots.
974-
size_t size_ = 0;
975-
976-
// The total number of available slots.
977992
size_t capacity_ = 0;
978993

979-
// Bundle together growth_left and HashtablezInfoHandle to ensure EBO for
994+
// Bundle together size and HashtablezInfoHandle to ensure EBO for
980995
// HashtablezInfoHandle when sampling is turned off.
981996
absl::container_internal::CompressedTuple<size_t, HashtablezInfoHandle>
982997
compressed_tuple_{0u, HashtablezInfoHandle{}};
@@ -1318,20 +1333,28 @@ inline void SetCtrl(const CommonFields& common, size_t i, h2_t h,
13181333
SetCtrl(common, i, static_cast<ctrl_t>(h), slot_size);
13191334
}
13201335

1336+
// growth_left (which is a size_t) is stored with the backing array.
1337+
constexpr size_t BackingArrayAlignment(size_t align_of_slot) {
1338+
return (std::max)(align_of_slot, alignof(size_t));
1339+
}
1340+
1341+
// Computes the offset from the start of the backing allocation of the control
1342+
// bytes. growth_left is stored at the beginning of the backing array.
1343+
inline size_t ControlOffset() { return sizeof(size_t); }
1344+
13211345
// Given the capacity of a table, computes the offset (from the start of the
13221346
// backing allocation) of the generation counter (if it exists).
13231347
inline size_t GenerationOffset(size_t capacity) {
13241348
assert(IsValidCapacity(capacity));
13251349
const size_t num_control_bytes = capacity + 1 + NumClonedBytes();
1326-
return num_control_bytes;
1350+
return ControlOffset() + num_control_bytes;
13271351
}
13281352

13291353
// Given the capacity of a table, computes the offset (from the start of the
13301354
// backing allocation) at which the slots begin.
13311355
inline size_t SlotOffset(size_t capacity, size_t slot_align) {
13321356
assert(IsValidCapacity(capacity));
1333-
const size_t num_control_bytes = capacity + 1 + NumClonedBytes();
1334-
return (num_control_bytes + NumGenerationBytes() + slot_align - 1) &
1357+
return (GenerationOffset(capacity) + NumGenerationBytes() + slot_align - 1) &
13351358
(~slot_align + 1);
13361359
}
13371360

@@ -1356,13 +1379,15 @@ ABSL_ATTRIBUTE_NOINLINE void InitializeSlots(CommonFields& c, Alloc alloc) {
13561379
: 0;
13571380

13581381
const size_t cap = c.capacity();
1382+
const size_t alloc_size = AllocSize(cap, SizeOfSlot, AlignOfSlot);
1383+
// growth_left (which is a size_t) is stored with the backing array.
13591384
char* mem = static_cast<char*>(
1360-
Allocate<AlignOfSlot>(&alloc, AllocSize(cap, SizeOfSlot, AlignOfSlot)));
1385+
Allocate<BackingArrayAlignment(AlignOfSlot)>(&alloc, alloc_size));
13611386
const GenerationType old_generation = c.generation();
13621387
c.set_generation_ptr(
13631388
reinterpret_cast<GenerationType*>(mem + GenerationOffset(cap)));
13641389
c.set_generation(NextGeneration(old_generation));
1365-
c.set_control(reinterpret_cast<ctrl_t*>(mem));
1390+
c.set_control(reinterpret_cast<ctrl_t*>(mem + ControlOffset()));
13661391
c.set_slots(mem + SlotOffset(cap, AlignOfSlot));
13671392
ResetCtrl(c, SizeOfSlot);
13681393
if (sample_size) {
@@ -1385,8 +1410,8 @@ struct PolicyFunctions {
13851410
void (*transfer)(void* set, void* dst_slot, void* src_slot);
13861411

13871412
// Deallocate the specified backing store which is sized for n slots.
1388-
void (*dealloc)(void* set, const PolicyFunctions& policy, ctrl_t* ctrl,
1389-
void* slot_array, size_t n);
1413+
void (*dealloc)(void* set, const PolicyFunctions& policy,
1414+
void* backing_array_start, void* slot_array, size_t n);
13901415
};
13911416

13921417
// ClearBackingArray clears the backing array, either modifying it in place,
@@ -1405,14 +1430,14 @@ void EraseMetaOnly(CommonFields& c, ctrl_t* it, size_t slot_size);
14051430
template <size_t AlignOfSlot>
14061431
ABSL_ATTRIBUTE_NOINLINE void DeallocateStandard(void*,
14071432
const PolicyFunctions& policy,
1408-
ctrl_t* ctrl, void* slot_array,
1409-
size_t n) {
1433+
void* backing_array_start,
1434+
void* slot_array, size_t n) {
14101435
// Unpoison before returning the memory to the allocator.
14111436
SanitizerUnpoisonMemoryRegion(slot_array, policy.slot_size * n);
14121437

14131438
std::allocator<char> alloc;
1414-
Deallocate<AlignOfSlot>(&alloc, ctrl,
1415-
AllocSize(n, policy.slot_size, AlignOfSlot));
1439+
Deallocate<BackingArrayAlignment(AlignOfSlot)>(
1440+
&alloc, backing_array_start, AllocSize(n, policy.slot_size, AlignOfSlot));
14161441
}
14171442

14181443
// For trivially relocatable types we use memcpy directly. This allows us to
@@ -1773,7 +1798,9 @@ class raw_hash_set {
17731798

17741799
raw_hash_set(const raw_hash_set& that, const allocator_type& a)
17751800
: raw_hash_set(0, that.hash_ref(), that.eq_ref(), a) {
1776-
reserve(that.size());
1801+
const size_t size = that.size();
1802+
if (size == 0) return;
1803+
reserve(size);
17771804
// Because the table is guaranteed to be empty, we can do something faster
17781805
// than a full `insert`.
17791806
for (const auto& v : that) {
@@ -1784,8 +1811,8 @@ class raw_hash_set {
17841811
common().maybe_increment_generation_on_insert();
17851812
infoz().RecordInsert(hash, target.probe_length);
17861813
}
1787-
common().set_size(that.size());
1788-
set_growth_left(growth_left() - that.size());
1814+
common().set_size(size);
1815+
set_growth_left(growth_left() - size);
17891816
}
17901817

17911818
ABSL_ATTRIBUTE_NOINLINE raw_hash_set(raw_hash_set&& that) noexcept(
@@ -1838,8 +1865,8 @@ class raw_hash_set {
18381865

18391866
// Unpoison before returning the memory to the allocator.
18401867
SanitizerUnpoisonMemoryRegion(slot_array(), sizeof(slot_type) * cap);
1841-
Deallocate<alignof(slot_type)>(
1842-
&alloc_ref(), control(),
1868+
Deallocate<BackingArrayAlignment(alignof(slot_type))>(
1869+
&alloc_ref(), common().backing_array_start(),
18431870
AllocSize(cap, sizeof(slot_type), alignof(slot_type)));
18441871

18451872
infoz().Unregister();
@@ -2470,8 +2497,8 @@ class raw_hash_set {
24702497
if (old_capacity) {
24712498
SanitizerUnpoisonMemoryRegion(old_slots,
24722499
sizeof(slot_type) * old_capacity);
2473-
Deallocate<alignof(slot_type)>(
2474-
&alloc_ref(), old_ctrl,
2500+
Deallocate<BackingArrayAlignment(alignof(slot_type))>(
2501+
&alloc_ref(), old_ctrl - ControlOffset(),
24752502
AllocSize(old_capacity, sizeof(slot_type), alignof(slot_type)));
24762503
}
24772504
infoz().RecordRehash(total_probe_length);
@@ -2707,15 +2734,15 @@ class raw_hash_set {
27072734
static_cast<slot_type*>(src));
27082735
}
27092736
// Note: dealloc_fn will only be used if we have a non-standard allocator.
2710-
static void dealloc_fn(void* set, const PolicyFunctions&, ctrl_t* ctrl,
2711-
void* slot_mem, size_t n) {
2737+
static void dealloc_fn(void* set, const PolicyFunctions&,
2738+
void* backing_array_start, void* slot_mem, size_t n) {
27122739
auto* h = static_cast<raw_hash_set*>(set);
27132740

27142741
// Unpoison before returning the memory to the allocator.
27152742
SanitizerUnpoisonMemoryRegion(slot_mem, sizeof(slot_type) * n);
27162743

2717-
Deallocate<alignof(slot_type)>(
2718-
&h->alloc_ref(), ctrl,
2744+
Deallocate<BackingArrayAlignment(alignof(slot_type))>(
2745+
&h->alloc_ref(), backing_array_start,
27192746
AllocSize(n, sizeof(slot_type), alignof(slot_type)));
27202747
}
27212748

0 commit comments

Comments
 (0)