The code contained RAW hazards where an 8-byte write was read as a single byte read causing a processor stall. Reworked the code to avoid this. PiperOrigin-RevId: 1002525484
diff --git a/absl/container/internal/raw_hash_set.h b/absl/container/internal/raw_hash_set.h index 83b547e..1393167 100644 --- a/absl/container/internal/raw_hash_set.h +++ b/absl/container/internal/raw_hash_set.h
@@ -746,11 +746,14 @@ class HashtableInlineData { // The number of bits in the seed. It is big enough to ensure // non-determinism of iteration order. We store the seed inside a uint64_t - // together with size and other metadata. The seed occupies the entire second - // byte of `data_` so that on the critical path of lookups it can be loaded - // with a single byte load and passed to the hash function without any - // masking. When absl::Hash is inlined, it can have lower latency knowing that - // the high bits of the seed are zero. + // together with size and other metadata. Reading `capacity` and `seed` by + // masking the 64-bit `data_` word (rather than issuing narrow byte loads) + // lets the compiler share a single 64-bit load across `is_small()`, + // `capacity()`, `seed()`, and `size()` (and pair it with `control_` via `ldp` + // on AArch64), while avoiding store-to-load forwarding (RAW) stalls after + // 64-bit stores to `data_` on both x86 and ARM. When absl::Hash is inlined, + // it can use the pre-shifted `data_ & kSeedMask` value directly without a + // right shift, knowing that the high bits of the seed are zero. static constexpr size_t kSeedBitCount = 8; public: @@ -759,6 +762,9 @@ static constexpr size_t kMaxBlockedElementCount = (uint64_t{1} << kBlockedElementBitCount) - 1; static constexpr size_t kCapacityBitCount = 6; + static constexpr size_t kHasInfozShift = kCapacityBitCount; + static constexpr size_t kSeedShift = + kHasInfozShift + /*has_infoz*/ 1 + /*unused*/ 1; static constexpr size_t kSizeBitCount = 64 - (kBlockedElementBitCount + kSeedBitCount + kGrowthInfoLowerBoundBitCount + @@ -773,12 +779,8 @@ (has_tried_sampling ? kSooHasTriedSamplingMask : 0)) {} HashtableCapacity capacity() const { - // Reading the capacity via a single byte load (rather than a wide load of - // `data_` followed by masking) allows the compiler to also use a single - // byte load for the seed on the critical path of lookups. The high bits of - // the byte are not part of the capacity. return HashtableCapacity::FromRawData( - static_cast<uint8_t>(read_byte(kCapacityByteIndex) & kCapacityMask)); + static_cast<uint8_t>(data_ & kCapacityMask)); } bool is_small() const { return capacity().is_small(); } @@ -815,12 +817,7 @@ (data_ & kMetadataMask) | (static_cast<uint64_t>(size) << kSizeShift); } - PerTableSeed seed() const { - // The seed is stored in exactly one byte, so we read it with a single byte - // load. This lets the compiler feed it directly into the hash function - // without a masking instruction on the critical path of lookups. - return PerTableSeed(read_byte(kSeedByteIndex)); - } + PerTableSeed seed() const { return PerTableSeed(data_ & kSeedMask); } void generate_new_seed() { set_seed(NextHashTableSeed()); } @@ -828,7 +825,7 @@ // hashes use the same seed and can e.g. identify stuck bits accurately. void set_sampled_seed() { set_seed(kSampledSeed); } - bool is_sampled_seed() const { return seed().seed() == kSampledSeed; } + bool is_sampled_seed() const { return seed().seed() == kSeedMask; } // Returns true if the table has infoz. bool has_infoz() const { @@ -916,20 +913,14 @@ // (1 bit) : has_infoz // (6 bits) : capacity (the low 6 bits of the first byte) // Note: for size to overflow would require ~2^40 bytes of RAM, which is - // impractical. The seed and the capacity are deliberately byte-aligned and - // accessed with byte loads: this lets lookups avoid a wide load plus masking - // on the critical path. We don't split these components of `data_` into - // separate bit field elements because we get worse generated code that way - // (the compiler loads the whole word and extracts the fields). + // impractical. We don't split these components of `data_` into separate bit + // field elements because we get worse generated code that way. static constexpr size_t kSizeShift = 64 - kSizeBitCount; static constexpr uint64_t kSizeOneNoMetadata = uint64_t{1} << kSizeShift; static constexpr uint64_t kMetadataMask = kSizeOneNoMetadata - 1; static constexpr uint64_t kCapacityMask = (1 << kCapacityBitCount) - 1; - static constexpr size_t kHasInfozShift = kCapacityBitCount; static constexpr uint64_t kHasInfozMask = uint64_t{1} << kHasInfozShift; - static constexpr size_t kSeedShift = - kHasInfozShift + /*has_infoz*/ 1 + /*unused*/ 1; static constexpr uint64_t kSeedMask = ((uint64_t{1} << kSeedBitCount) - 1) << kSeedShift; static constexpr uint64_t kBlockedElementsShift = kSeedShift + kSeedBitCount; @@ -952,25 +943,8 @@ // hashes use the same seed and can e.g. identify stuck bits accurately. static constexpr uint8_t kSampledSeed = (1 << kSeedBitCount) - 1; - // Indices of the bytes of `data_` that hold the capacity and the seed. static_assert(kSeedShift == 8); static_assert(kSeedBitCount == 8); -#ifdef ABSL_IS_LITTLE_ENDIAN - static constexpr size_t kCapacityByteIndex = 0; - static constexpr size_t kSeedByteIndex = 1; -#else - static constexpr size_t kCapacityByteIndex = 7; - static constexpr size_t kSeedByteIndex = 6; -#endif - - // Returns the `index`-th byte of `data_`. Accessing the storage through - // `unsigned char` is always allowed by the aliasing rules. Note: the reason - // we read the data this way rather than using bit manipulations as with other - // fields is that it results in better generated code (we get movzbl to read - // the seed this way). - uint8_t read_byte(size_t index) const { - return reinterpret_cast<const unsigned char*>(&data_)[index]; - } // Returns the bits of `data_` that encode `capacity`. static uint64_t CapacityBits(HashtableCapacity capacity) { @@ -2500,10 +2474,10 @@ // For non-default hashers the seed is xor'ed with the hash value rather than // being used as a seed for the hash function. Because we require random // iteration order for small tables, the seed must have entropy in its low - // bits. The seed is loaded as a single byte from the inline data, so it - // already has low bits entropy and no shift is needed. - // TODO(ezb): get rid of kSeedShift. - constexpr static size_t kSeedShift = 0; + // bits. For default absl::Hash, the pre-shifted `data_ & kSeedMask` value can + // be passed directly without a right shift. + constexpr static size_t kSeedShift = + kIsAbslHash ? 0 : HashtableInlineData::kSeedShift; constexpr static bool SooEnabled() { return PolicyTraits::soo_enabled() &&