| // Copyright 2025 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #ifndef PARTITION_ALLOC_BUCKET_LOOKUP_H_ |
| #define PARTITION_ALLOC_BUCKET_LOOKUP_H_ |
| |
| #include <array> |
| #include <cstdint> |
| #include <limits> |
| #include <type_traits> |
| #include <utility> |
| |
| #include "partition_alloc/buildflags.h" |
| #include "partition_alloc/partition_alloc_base/bits.h" |
| #include "partition_alloc/partition_alloc_base/compiler_specific.h" |
| #include "partition_alloc/partition_alloc_check.h" |
| #include "partition_alloc/partition_alloc_forward.h" |
| |
| // `BucketIndexLookup` class provides 2-way mapping between "allocation size" |
| // and "bucket index". |
| // https://chromium.googlesource.com/chromium/src/+/HEAD/base/allocator/partition_allocator/buckets.md |
| // |
| // We have two different mappings; Neutral Bucket |
| // Distribution and Denser Bucket Distribution. As the name implies, Denser one |
| // has about twice as many buckets. Neutral Bucket Distribution leaves some |
| // buckets unused. This structure allows us to switch from Neutral to Denser at |
| // runtime easily. To simplify implementation, Neutral is implemented by |
| // rounding up indices from Denser (see `GetIndexForNeutralBuckets()`). |
| // |
| // Denser distribution is mixture of linear and exponential curve. |
| // For small size, we have a bucket for every `kAlignment` bytes linearly. |
| // For larger size, we have `kNumBucketsPerOrder` buckets for every power |
| // of two ("order"), exponentially. |
| // |
| // The linear curve and the exponential curve are implemented as |
| // `LinearBucketMapping` and `ExponentialBucketMapping` respectively, and merged |
| // in `BucketIndexLookup`. |
| // |
| // Constants in this file must be kept in sync with |
| // //tools/memory/partition_allocator/objects_per_size.py. |
| // LINT.IfChange |
| |
| namespace partition_alloc { |
| |
| namespace internal { |
| |
| class LinearBucketMapping final { |
| public: |
| static constexpr size_t kStep = internal::kAlignment; |
| |
| LinearBucketMapping() = delete; |
| |
| PA_ALWAYS_INLINE static constexpr size_t GetIndex(size_t size) { |
| return size == 0 ? 0 : (size - 1) / kStep; |
| } |
| |
| PA_ALWAYS_INLINE static constexpr size_t GetSize(uint16_t index) { |
| return (static_cast<size_t>(index) + 1) * kStep; |
| } |
| }; |
| |
| class ExponentialBucketMapping final { |
| public: |
| // 8 buckets per order (for the higher orders). |
| // Note: this is not what is used by neutral distribution, but the maximum |
| // amount of buckets per order. For neutral distribution, only 4 are used. |
| static constexpr size_t kNumBucketsPerOrderBits = 3; |
| static constexpr size_t kNumBucketsPerOrder = 1 << kNumBucketsPerOrderBits; |
| |
| ExponentialBucketMapping() = delete; |
| |
| PA_ALWAYS_INLINE static constexpr size_t GetIndex(size_t size) { |
| // The "order" of an allocation is closely related to the power-of-2 size of |
| // the allocation. More precisely, the order is the bit index of the |
| // most-significant-bit in the allocation size, where the bit numbers starts |
| // at index 1 for the least-significant-bit. |
| // |
| // Obtain index of MSB and rotate to extract Order Indices. |
| // |
| // ┌──────── Order: 8 |
| // │ ┌────── Order Index: 5 |
| // │┌┴┐┌──┬─ Order Sub-Index: true |
| // Size 216 = 0b00000000000000000000000011011000 |
| // 32......................987654321 (n-th bit, 1-indexed) |
| // After RotR = 0b10010000000000000000000000001101 |
| // └─────────────────────────┬┘ └─┴─ Order Index |
| // └────── Order Sub-Index |
| // |
| // This rotation allows to extract indices with compile-time constant |
| // masks. |
| const size_t order = |
| kBitsPerSizeT - |
| static_cast<size_t>(internal::base::bits::CountlZero(size)); |
| const size_t rot = internal::base::bits::RotR( |
| size, order - kNumBucketsPerOrderBits + kBitsPerSizeT - 1); |
| |
| // Index is the lowest `kNumBucketsPerOrderBits` bits after rotation. |
| constexpr size_t kIndexMask = (size_t{1} << kNumBucketsPerOrderBits) - 1; |
| const size_t order_index = rot & kIndexMask; |
| |
| // Sub-Index is the highest `kBitsPerSizeT - kNumBucketsPerOrderBits - 1` |
| // bits after rotation. If it is non-zero, we should increase index by |
| // one. |
| constexpr size_t kSubIndexMask = |
| ~((size_t{1} << (kNumBucketsPerOrderBits + 1)) - 1); |
| const size_t sub_order_index = !!(rot & kSubIndexMask); |
| |
| return order * kNumBucketsPerOrder + order_index + sub_order_index; |
| } |
| |
| PA_ALWAYS_INLINE static constexpr size_t GetSize(uint16_t index) { |
| const size_t order = index / kNumBucketsPerOrder; |
| const size_t order_index = index % kNumBucketsPerOrder; |
| |
| size_t size = kNumBucketsPerOrder | order_index; |
| size <<= order == 0 ? 0 : order - 1 - kNumBucketsPerOrderBits; |
| |
| return size; |
| } |
| }; |
| } // namespace internal |
| |
| class BucketIndexLookup final { |
| using LinearMap = internal::LinearBucketMapping; |
| using ExponentialMap = internal::ExponentialBucketMapping; |
| |
| // PartitionAlloc should return memory properly aligned for any type, to |
| // behave properly as a generic allocator. This is not strictly required as |
| // long as types are explicitly allocated with PartitionAlloc, but is to use |
| // it as a malloc() implementation, and generally to match malloc()'s |
| // behavior. In practice, this means 8 bytes alignment on 32 bit |
| // architectures, and 16 bytes on 64 bit ones. We use linear curve iff |
| // `size` is too small for exponential distribution to violate fundamental |
| // alignment. |
| // |
| // For size no greater than `kMaxLinear`, `LinearMap` is used. For size no |
| // less than `kMinExponential`, `ExponentialMap` is used. There is small |
| // overlap between linear and exponential. |
| // |
| // LinearMap | <-> | <------------> | |
| // ExponentialMap | | <------------> | <--------> | |
| // ^ ^ ^ ^ |
| // 0 kMinExponential kMaxLinear kMaxBucketSize |
| |
| static constexpr size_t kMinExponential = |
| LinearMap::kStep << ExponentialMap::kNumBucketsPerOrderBits; |
| static constexpr size_t kMaxLinear = |
| LinearMap::kStep << (ExponentialMap::kNumBucketsPerOrderBits + 1); |
| static_assert(kMinExponential < kMaxLinear); |
| |
| // There is a gap between Linear's index and Exponential's index at |
| // `kMinExponential`. To reduce waste by holes, offset exponential index to |
| // make "smooth" curve. |
| static constexpr size_t kExponentialIndexOffset = |
| ExponentialMap::GetIndex(kMinExponential) - |
| LinearMap::GetIndex(kMinExponential); |
| static_assert(kExponentialIndexOffset == |
| ExponentialMap::GetIndex(kMaxLinear) - |
| LinearMap::GetIndex(kMaxLinear)); |
| |
| public: |
| BucketIndexLookup() = delete; |
| |
| static constexpr size_t kMinBucketSize = LinearMap::kStep; |
| // The largest bucketed order is 20, storing nearly 1 MiB (983040 bytes |
| // precisely). |
| static constexpr size_t kMaxBucketSize = ExponentialMap::GetSize( |
| (20 + 1) * ExponentialMap::kNumBucketsPerOrder - 1); |
| |
| static constexpr uint16_t kNumBuckets = static_cast<uint16_t>( |
| ExponentialMap::GetIndex(kMaxBucketSize) - kExponentialIndexOffset + 1); |
| |
| PA_ALWAYS_INLINE static constexpr uint16_t GetIndexForDenserBuckets( |
| size_t size) { |
| size_t index_if_linear = LinearMap::GetIndex(size); |
| size_t index_if_exponential = |
| std::min(ExponentialMap::GetIndex(size) - kExponentialIndexOffset, |
| size_t{kNumBuckets}); |
| |
| // Ternary operator will likely to be compiled as conditional move. |
| size_t index = size <= kMaxLinear ? index_if_linear : index_if_exponential; |
| |
| // Last one is the sentinel bucket. |
| PA_DCHECK(index <= kNumBuckets); |
| return static_cast<uint16_t>(index); |
| } |
| |
| PA_ALWAYS_INLINE static constexpr uint16_t GetIndexForNeutralBuckets( |
| size_t size) { |
| uint16_t index = GetIndexForDenserBuckets(size); |
| // Below the minimum size, 4 and 8 bucket distributions are the same, since |
| // we can't fit any more buckets per order; this is due to alignment |
| // requirements: each bucket must be a multiple of the alignment, which |
| // implies the difference between buckets must also be a multiple of the |
| // alignment. In smaller orders, this limits the number of buckets we can |
| // have per order. So, for these small order, we do not want to skip every |
| // second bucket. |
| // |
| // We also do not want to go about the index for the max bucketed size. |
| if (size >= kMaxLinear && index + 1 < kNumBuckets) { |
| index += (index % 2 == 0); |
| } |
| return index; |
| } |
| |
| PA_ALWAYS_INLINE static constexpr size_t GetBucketSize(uint16_t index) { |
| PA_DCHECK(index < kNumBuckets); |
| |
| constexpr size_t kMaxLinearIndex = LinearMap::GetIndex(kMaxLinear); |
| size_t size_if_linear = LinearMap::GetSize(index); |
| size_t size_if_exponential = |
| ExponentialMap::GetSize(index + kExponentialIndexOffset); |
| |
| // Ternary operator will likely to be compiled as conditional move. |
| return index <= kMaxLinearIndex ? size_if_linear : size_if_exponential; |
| } |
| |
| PA_CONSTINIT static const std::array<size_t, kNumBuckets> kBucketSizes; |
| }; |
| |
| } // namespace partition_alloc |
| |
| // LINT.ThenChange(//tools/memory/partition_allocator/objects_per_size.py) |
| |
| #endif // PARTITION_ALLOC_BUCKET_LOOKUP_H_ |