[llvm] [DenseMap] Share rehash and grow for relocatable bucket types. NFC (PR #225018)
Fangrui Song via llvm-commits
llvm-commits at lists.llvm.org
Sat Sep 26 23:35:58 PDT 2026
MaskRay wrote:
> Nice compile-time and binary-size wins!
>
> Could we avoid the runtime `if (!Hasher)` and `switch (BucketSize)` in `DenseMap.cpp` by templating `growRelocatable` / `rehashRelocatable` (or a base class in the spirit of `SmallPtrSetImpl`) on `<FixedSize, InlinePtrHash>` and explicitly instantiating them in `DenseMap.cpp` for the 8 common sizes (`4, 8, 12, 16, 24, 32, 40, 48`)?
>
> In `DenseMapBase::grow`, `if constexpr` already separates relocatable buckets from non-relocatable ones:
>
> ```c++
> if constexpr (isRelocatableBucket<BucketT> && isCommonBucketSize<sizeof(BucketT)>) {
> // Calls growRelocatable<sizeof(BucketT), InlinePtrHash>(...) directly
> derived().growShared(MinNumBuckets);
> } else {
> // Existing inline Tmp.moveFrom(...) path handles non-relocatable buckets
> // and uncommon bucket sizes
> ...
> }
> ```
>
> * **No runtime `switch (BucketSize)` or `if (!Hasher)`**: Common relocatable buckets (95% of maps) jump directly to `growRelocatable<16, true>` at compile time without a runtime switch or jump table (avoiding the issue @aengelke noted).
> * **Clean fallback**: Non-relocatable buckets and uncommon bucket sizes simply fall into the existing `else` branch (or uncommon relocatable sizes can map `FixedSize` to `0` at compile time and call `growRelocatable<0, InlinePtrHash>`).
The switch and the `!Hasher` test runs once per grow. The runtime overhead is minimal is unmeasurable in my previous experiments. I'd also like to avoid `extern template`:
```cpp
#define DENSEMAP_RELOCATABLE(N, B) \
extern template LLVM_TEMPLATE_ABI void rehashRelocatable<N, B>( \
void *, UsedT *, unsigned, const void *, const UsedT *, unsigned, \
size_t, BucketHasher); \
extern template LLVM_TEMPLATE_ABI void *growRelocatable<N, B>( \
void *, const UsedT *, unsigned, unsigned, size_t, size_t, BucketHasher, \
bool);
#define DENSEMAP_RELOCATABLE_SIZE(N) \
DENSEMAP_RELOCATABLE(N, false) DENSEMAP_RELOCATABLE(N, true)
DENSEMAP_RELOCATABLE_SIZE(0)
DENSEMAP_RELOCATABLE_SIZE(4)
DENSEMAP_RELOCATABLE_SIZE(8)
DENSEMAP_RELOCATABLE_SIZE(12)
DENSEMAP_RELOCATABLE_SIZE(16)
DENSEMAP_RELOCATABLE_SIZE(24)
DENSEMAP_RELOCATABLE_SIZE(32)
DENSEMAP_RELOCATABLE_SIZE(40)
DENSEMAP_RELOCATABLE_SIZE(48)
```
> * **Sharing beyond `grow`**: If we eventually move this into a `SmallPtrSetImpl`-style base class parameterized on `<BucketSize>` for pointer keys (`InlinePtrHash == true`), all 16-byte pointer-keyed maps (`DenseMap<Instruction *, unsigned>`, `DenseMap<BasicBlock *, int>`, `DenseMap<Value *, Value *>`, etc.) could also share `doFind`, `LookupBucketFor`, `findBucketForInsertion`, and `copyFrom` rather than instantiating them per `(KeyT, ValueT)` type.
For an initially empty DenseMap, one call to `growRelocatable` is spread over at least 48 moved entries (3/4 * 64 = 48). A jump table or a depth-4 compare tree overhead is minimal compared with 48 moved entries.
Sharing lookups is a different trade: the call is paid per operation, the hash function is called. Whether the hash function and `doFind` are inlined or not has a larger performance difference.
https://github.com/llvm/llvm-project/pull/225018
More information about the llvm-commits
mailing list