[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