Optimize GraphCycles::PointerMap and inline hot GraphCycles helpers.
GraphCycles::PointerMap previously used a 262,139-entry (1 MiB) int32_t bucket array chained through Node::next_hash, filling 1 MiB on GraphCycles construction and dereferencing rep_->nodes_[index]->masked_ptr across separate allocations on every probe.
Replace PointerMap with a dedicated flat open-addressed table storing {masked_ptr, GraphId} pairs inline with an initial capacity of 1024 entries, and mark hot GraphCycles accessors (Vec::operator[], NodeSet::FindIndex, NodeSet::Next, PointerMap::Find, FindNode, and ByRank::operator()) ABSL_ATTRIBUTE_ALWAYS_INLINE so unoptimized and non-LTO builds do not pay function call overhead on every edge traversal.
PiperOrigin-RevId: 988680840
diff --git a/absl/synchronization/internal/graphcycles.cc b/absl/synchronization/internal/graphcycles.cc
index 7079081..fc870f4 100644
--- a/absl/synchronization/internal/graphcycles.cc
+++ b/absl/synchronization/internal/graphcycles.cc
@@ -36,7 +36,6 @@
#ifndef ABSL_LOW_LEVEL_ALLOC_MISSING
#include <algorithm>
-#include <array>
#include <cinttypes>
#include <cstddef>
#include <cstdint>
@@ -92,11 +91,13 @@
}
bool empty() const { return size_ == 0; }
- uint32_t size() const { return size_; }
+ ABSL_ATTRIBUTE_ALWAYS_INLINE uint32_t size() const { return size_; }
T* begin() { return ptr_; }
T* end() { return ptr_ + size_; }
- const T& operator[](uint32_t i) const { return ptr_[i]; }
- T& operator[](uint32_t i) { return ptr_[i]; }
+ ABSL_ATTRIBUTE_ALWAYS_INLINE const T& operator[](uint32_t i) const {
+ return ptr_[i];
+ }
+ ABSL_ATTRIBUTE_ALWAYS_INLINE T& operator[](uint32_t i) { return ptr_[i]; }
const T& back() const { return ptr_[size_ - 1]; }
void pop_back() { size_--; }
@@ -172,7 +173,9 @@
NodeSet() { Init(); }
void clear() { Init(); }
- bool contains(int32_t v) const { return table_[FindIndex(v)] == v; }
+ ABSL_ATTRIBUTE_ALWAYS_INLINE bool contains(int32_t v) const {
+ return table_[FindIndex(v)] == v;
+ }
bool insert(int32_t v) {
uint32_t i = FindIndex(v);
@@ -202,7 +205,7 @@
// HASH_FOR_EACH(elem, node->out) { ... }
#define HASH_FOR_EACH(elem, eset) \
for (int32_t elem, _cursor = 0; (eset).Next(&_cursor, &elem);)
- bool Next(int32_t* cursor, int32_t* elem) {
+ ABSL_ATTRIBUTE_ALWAYS_INLINE bool Next(int32_t* cursor, int32_t* elem) {
while (static_cast<uint32_t>(*cursor) < table_.size()) {
int32_t v = table_[static_cast<uint32_t>(*cursor)];
(*cursor)++;
@@ -219,10 +222,12 @@
Vec<int32_t> table_;
uint32_t occupied_; // Count of non-empty slots (includes deleted slots)
- static uint32_t Hash(int32_t a) { return static_cast<uint32_t>(a) * 41; }
+ ABSL_ATTRIBUTE_ALWAYS_INLINE static uint32_t Hash(int32_t a) {
+ return static_cast<uint32_t>(a) * 41;
+ }
// Return index for storing v. May return an empty index or deleted index
- uint32_t FindIndex(int32_t v) const {
+ ABSL_ATTRIBUTE_ALWAYS_INLINE uint32_t FindIndex(int32_t v) const {
// Search starting at hash index.
const uint32_t mask = table_.size() - 1;
uint32_t i = Hash(v) & mask;
@@ -271,16 +276,19 @@
// number is incremented when the GraphId is freed which automatically
// invalidates all copies of the GraphId.
-inline GraphId MakeId(int32_t index, uint32_t version) {
+ABSL_ATTRIBUTE_ALWAYS_INLINE inline GraphId MakeId(int32_t index,
+ uint32_t version) {
GraphId g;
g.handle =
(static_cast<uint64_t>(version) << 32) | static_cast<uint32_t>(index);
return g;
}
-inline int32_t NodeIndex(GraphId id) { return static_cast<int32_t>(id.handle); }
+ABSL_ATTRIBUTE_ALWAYS_INLINE inline int32_t NodeIndex(GraphId id) {
+ return static_cast<int32_t>(id.handle);
+}
-inline uint32_t NodeVersion(GraphId id) {
+ABSL_ATTRIBUTE_ALWAYS_INLINE inline uint32_t NodeVersion(GraphId id) {
return static_cast<uint32_t>(id.handle >> 32);
}
@@ -289,7 +297,6 @@
int32_t prev_slot; // Slot with the next lower rank, or -1 if none
int32_t next_slot; // Slot with the next higher rank, or -1 if none
uint32_t version; // Current version number
- int32_t next_hash; // Next entry in hash table
bool visited; // Temporary marker used by depth-first-search
uintptr_t masked_ptr; // User-supplied pointer
NodeSet in; // List of immediate predecessor nodes in graph
@@ -299,56 +306,119 @@
void* stack[40]; // stack[0,nstack-1] holds stack trace for node.
};
-// Hash table for pointer to node index lookups.
+// Hash table for pointer to GraphId lookups.
class PointerMap {
public:
- explicit PointerMap(const Vec<Node*>* nodes) : nodes_(nodes) {
- table_.fill(-1);
- }
+ PointerMap() { Init(kInitialCapacity); }
+ ~PointerMap() { base_internal::LowLevelAlloc::Free(table_); }
- int32_t Find(void* ptr) {
- auto masked = base_internal::HidePtr(ptr);
- for (int32_t i = table_[Hash(ptr)]; i != -1;) {
- Node* n = (*nodes_)[static_cast<uint32_t>(i)];
- if (n->masked_ptr == masked) return i;
- i = n->next_hash;
+ ABSL_ATTRIBUTE_ALWAYS_INLINE GraphId Find(void* ptr) const {
+ const uintptr_t masked = base_internal::HidePtr(ptr);
+ const uint32_t mask = capacity_ - 1;
+ uint32_t i = Hash(masked) & mask;
+ while (true) {
+ const Entry& e = table_[i];
+ if (e.id.handle == kEmptyHandle) return InvalidGraphId();
+ if (e.id.handle != kDelHandle && e.masked_ptr == masked) return e.id;
+ i = (i + 1) & mask;
}
- return -1;
}
- void Add(void* ptr, int32_t i) {
- int32_t* head = &table_[Hash(ptr)];
- (*nodes_)[static_cast<uint32_t>(i)]->next_hash = *head;
- *head = i;
+ void Add(void* ptr, GraphId id) {
+ if (occupied_ >= capacity_ - capacity_ / 4) {
+ Rehash(live_ >= capacity_ / 2 ? capacity_ * 2 : capacity_);
+ }
+ const uintptr_t masked = base_internal::HidePtr(ptr);
+ const uint32_t mask = capacity_ - 1;
+ uint32_t i = Hash(masked) & mask;
+ while (table_[i].id.handle != kEmptyHandle &&
+ table_[i].id.handle != kDelHandle) {
+ i = (i + 1) & mask;
+ }
+ if (table_[i].id.handle == kEmptyHandle) {
+ occupied_++;
+ }
+ live_++;
+ table_[i].masked_ptr = masked;
+ table_[i].id = id;
}
int32_t Remove(void* ptr) {
- // Advance through linked list while keeping track of the
- // predecessor slot that points to the current entry.
- auto masked = base_internal::HidePtr(ptr);
- for (int32_t* slot = &table_[Hash(ptr)]; *slot != -1;) {
- int32_t index = *slot;
- Node* n = (*nodes_)[static_cast<uint32_t>(index)];
- if (n->masked_ptr == masked) {
- *slot = n->next_hash; // Remove n from linked list
- n->next_hash = -1;
+ const uintptr_t masked = base_internal::HidePtr(ptr);
+ const uint32_t mask = capacity_ - 1;
+ uint32_t i = Hash(masked) & mask;
+ while (true) {
+ Entry& e = table_[i];
+ if (e.id.handle == kEmptyHandle) return -1;
+ if (e.id.handle != kDelHandle && e.masked_ptr == masked) {
+ const int32_t index = NodeIndex(e.id);
+ live_--;
+ if (table_[(i + 1) & mask].id.handle == kEmptyHandle) {
+ e.id.handle = kEmptyHandle;
+ occupied_--;
+ } else {
+ e.id.handle = kDelHandle;
+ }
return index;
}
- slot = &n->next_hash;
+ i = (i + 1) & mask;
}
- return -1;
}
private:
- // Number of buckets in hash table for pointer lookups.
- static constexpr uint32_t kHashTableSize = 262139; // should be prime
+ static constexpr uint32_t kInitialCapacity = 1024;
+ static constexpr uint64_t kEmptyHandle = 0;
+ static constexpr uint64_t kDelHandle = 1;
- const Vec<Node*>* nodes_;
- std::array<int32_t, kHashTableSize> table_;
+ struct Entry {
+ uintptr_t masked_ptr;
+ GraphId id;
+ };
- static uint32_t Hash(void* ptr) {
- return reinterpret_cast<uintptr_t>(ptr) % kHashTableSize;
+ Entry* table_;
+ uint32_t capacity_;
+ uint32_t occupied_;
+ uint32_t live_;
+
+ ABSL_ATTRIBUTE_ALWAYS_INLINE static uint32_t Hash(uintptr_t masked) {
+ return static_cast<uint32_t>((masked * uint64_t{0x9e3779b97f4a7c15ULL}) >>
+ 32);
}
+
+ void Init(uint32_t capacity) {
+ capacity_ = capacity;
+ occupied_ = 0;
+ live_ = 0;
+ const size_t bytes = static_cast<size_t>(capacity) * sizeof(Entry);
+ table_ = static_cast<Entry*>(
+ base_internal::LowLevelAlloc::AllocWithArena(bytes, arena));
+ for (uint32_t i = 0; i < capacity; ++i) {
+ table_[i].id.handle = kEmptyHandle;
+ }
+ }
+
+ void Rehash(uint32_t new_capacity) {
+ Entry* old_table = table_;
+ const uint32_t old_capacity = capacity_;
+ Init(new_capacity);
+ const uint32_t mask = new_capacity - 1;
+ for (uint32_t j = 0; j < old_capacity; ++j) {
+ const Entry& old_e = old_table[j];
+ if (old_e.id.handle != kEmptyHandle && old_e.id.handle != kDelHandle) {
+ uint32_t i = Hash(old_e.masked_ptr) & mask;
+ while (table_[i].id.handle != kEmptyHandle) {
+ i = (i + 1) & mask;
+ }
+ table_[i] = old_e;
+ occupied_++;
+ live_++;
+ }
+ }
+ base_internal::LowLevelAlloc::Free(old_table);
+ }
+
+ PointerMap(const PointerMap&) = delete;
+ PointerMap& operator=(const PointerMap&) = delete;
};
} // namespace
@@ -400,11 +470,10 @@
Vec<int64_t> sorted_ranks;
Vec<int32_t> old_prev_slots;
Vec<int32_t> old_next_slots;
-
- Rep() : ptrmap_(&nodes_) {}
};
-static Node* FindNode(GraphCycles::Rep* rep, GraphId id) {
+ABSL_ATTRIBUTE_ALWAYS_INLINE static Node* FindNode(GraphCycles::Rep* rep,
+ GraphId id) {
Node* n = rep->nodes_[static_cast<uint32_t>(NodeIndex(id))];
return (n->version == NodeVersion(id)) ? n : nullptr;
}
@@ -560,7 +629,8 @@
for (uint32_t x = 0; x < r->nodes_.size(); x++) {
Node* nx = r->nodes_[x];
void* ptr = base_internal::UnhidePtr<void>(nx->masked_ptr);
- if (ptr != nullptr && static_cast<uint32_t>(r->ptrmap_.Find(ptr)) != x) {
+ if (ptr != nullptr &&
+ r->ptrmap_.Find(ptr) != MakeId(static_cast<int32_t>(x), nx->version)) {
ABSL_RAW_LOG(FATAL, "Did not find live node in hash table %" PRIu32 " %p",
x, ptr);
}
@@ -609,9 +679,9 @@
}
GraphId GraphCycles::GetId(void* ptr) {
- int32_t i = rep_->ptrmap_.Find(ptr);
- if (i != -1) {
- return MakeId(i, rep_->nodes_[static_cast<uint32_t>(i)]->version);
+ GraphId id = rep_->ptrmap_.Find(ptr);
+ if (id != InvalidGraphId()) {
+ return id;
} else if (rep_->free_nodes_.empty()) {
const int32_t slot = static_cast<int32_t>(rep_->nodes_.size());
Node* n =
@@ -624,8 +694,9 @@
n->priority = 0;
rep_->nodes_.push_back(n);
LinkBetween(rep_, slot, rep_->highest_slot, /*next_slot=*/-1);
- rep_->ptrmap_.Add(ptr, slot);
- return MakeId(slot, n->version);
+ GraphId new_id = MakeId(slot, n->version);
+ rep_->ptrmap_.Add(ptr, new_id);
+ return new_id;
} else {
// The slot keeps its rank (see "Rank allocation").
int32_t slot = rep_->free_nodes_.back();
@@ -634,8 +705,9 @@
n->masked_ptr = base_internal::HidePtr(ptr);
n->nstack = 0;
n->priority = 0;
- rep_->ptrmap_.Add(ptr, slot);
- return MakeId(slot, n->version);
+ GraphId new_id = MakeId(slot, n->version);
+ rep_->ptrmap_.Add(ptr, new_id);
+ return new_id;
}
}
@@ -656,6 +728,7 @@
x->masked_ptr = base_internal::HidePtr<void>(nullptr);
if (x->version == std::numeric_limits<uint32_t>::max()) {
// Cannot use x any more
+ x->version = 0;
} else {
x->version++; // Invalidates all copies of node.
rep_->free_nodes_.push_back(i);
@@ -890,7 +963,7 @@
static void Sort(const Vec<Node*>& nodes, Vec<int32_t>* delta) {
struct ByRank {
const Vec<Node*>* nodes;
- bool operator()(int32_t a, int32_t b) const {
+ ABSL_ATTRIBUTE_ALWAYS_INLINE bool operator()(int32_t a, int32_t b) const {
return (*nodes)[static_cast<uint32_t>(a)]->rank <
(*nodes)[static_cast<uint32_t>(b)]->rank;
}