mirror of
https://github.com/carbon-language/carbon-lang.git
synced 2026-11-14 18:18:43 +00:00
Hashtables seed every hash with the address of a global,
`ComputeSeed()`, so that ASLR varies layout and iteration order between
runs. Debug builds already vary iteration order directly in `entries()`,
so the seed only matters in release builds. There it costs a register
and an XOR on the latency path of every integer or pointer hash, and it
redraws a table's collisions in each process.
Remove the seed from `HashValue`, `Hasher`, `CarbonHashValue`,
`CustomHashValue::Hash`, and `HashKey`. Hashing starts from a zero
buffer, so integer keys hash as `WeakMix(key)`. `global_addr_seed`
remains in debug builds to seed the iteration-order entropy.
`ClangDeclSignature` chained its hash through the seed, and now hashes
its fields with one `Hasher`.
The seed also kept some short-input paths from collapsing. Without it,
every `{i, 0, 0}` array of `int32_t` hashed the same, all-zero 4-8 byte
inputs lost their size, and the 17-32 byte path multiplied the size
sample by a byte-shifted copy of itself. Each `Mix` operand of the
short-input paths now XORs in a 32-bit constant from
`ImmediateRandomData` in place of a 64-bit chunk of `StaticRandomData`,
which they now only sample to encode sizes. `XorImmediate` keeps these
as `xor $imm32` on x86-64; without its `asm` barrier, a read feeding the
XOR is folded in instead and the constant ends up in a register that
loops hoist.
Cycles against trunk on Zen 5, from `bench_runner.py` with 48 runs per
side, each run at a random code layout, and table sizes of 64, 3584, and
57344:
- Integer and pointer hits: -3.0% to -5.7% in `Map`, -0.8% to -2.4% in
`Set`.
- Integer and pointer misses: up to -12.6%, except `Set<int*>` at +1.4%
(64) and +6.1% (3584).
- String keys: -2.8% to +2.1%, with `Map` updates at +1.1% to +1.6%.
- Hash latency, without layout randomization: integers and pointers
-9.8%, empty strings +5.4% (0.1 cycles), other strings not significant.
Same method on an Apple M1, with #7870 on both sides. Without it, this
change moves benchmark loop counters between `sp`- and `x29`-relative
stack slots, which alone makes `Set<int>` lookups up to 49% slower on
the M1.
- Integer and pointer hits: -1.0% to -10.3% in `Map`, -24.5% to +1.2% in
`Set`.
- Integer and pointer misses: -3.9% to -48.0%, except +7.2% to +59.3% at
3584, from longer probes (instructions +6.4% to +70.6%).
- Inserting 64 integer keys: +12.1% to +15.0%, from store memory-order
violations.
- String keys: -6.6% to +3.9%.
- Hash latency: integers and pointers -9.0% to -10.0%, empty strings
+48.8% (1 cycle).
Release builds now iterate a given table in the same order in every run.
Code that depends on that order still fails in debug builds.
Assisted-by: Claude Code
215 lines
7.8 KiB
C++
215 lines
7.8 KiB
C++
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
|
|
// Exceptions. See /LICENSE for license information.
|
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
|
|
#include "common/hashtable_key_context.h"
|
|
|
|
#include <gmock/gmock.h>
|
|
#include <gtest/gtest.h>
|
|
|
|
#include "common/hashing_llvm.h"
|
|
|
|
namespace Carbon {
|
|
namespace {
|
|
|
|
using ::testing::Eq;
|
|
using ::testing::Ne;
|
|
|
|
struct DefaultEq {
|
|
int x, y;
|
|
|
|
friend auto operator==(const DefaultEq& lhs, const DefaultEq& rhs)
|
|
-> bool = default;
|
|
};
|
|
|
|
struct CustomEq {
|
|
int x, y;
|
|
|
|
friend auto operator==(const CustomEq& lhs, const CustomEq& rhs) -> bool {
|
|
return lhs.x == rhs.x && lhs.y == rhs.y;
|
|
}
|
|
};
|
|
|
|
struct CustomExtEq {
|
|
int x, y;
|
|
|
|
friend auto CarbonHashtableEq(const CustomExtEq& lhs, const CustomExtEq& rhs)
|
|
-> bool {
|
|
return lhs.x == rhs.x && lhs.y == rhs.y;
|
|
}
|
|
};
|
|
|
|
TEST(HashtableKeyContextTest, HashtableEq) {
|
|
EXPECT_TRUE(HashtableEq(0, 0));
|
|
EXPECT_FALSE(HashtableEq(1, 0));
|
|
EXPECT_FALSE(HashtableEq(0, 1));
|
|
EXPECT_FALSE(HashtableEq(1234, 5678));
|
|
EXPECT_TRUE(HashtableEq(5678, 5678));
|
|
|
|
EXPECT_TRUE(
|
|
HashtableEq(DefaultEq{.x = 0, .y = 0}, DefaultEq{.x = 0, .y = 0}));
|
|
EXPECT_FALSE(
|
|
HashtableEq(DefaultEq{.x = 1, .y = 2}, DefaultEq{.x = 3, .y = 4}));
|
|
|
|
EXPECT_TRUE(HashtableEq(CustomEq{.x = 0, .y = 0}, CustomEq{.x = 0, .y = 0}));
|
|
EXPECT_FALSE(HashtableEq(CustomEq{.x = 1, .y = 2}, CustomEq{.x = 3, .y = 4}));
|
|
|
|
EXPECT_TRUE(
|
|
HashtableEq(CustomExtEq{.x = 0, .y = 0}, CustomExtEq{.x = 0, .y = 0}));
|
|
EXPECT_FALSE(
|
|
HashtableEq(CustomExtEq{.x = 1, .y = 2}, CustomExtEq{.x = 3, .y = 4}));
|
|
}
|
|
|
|
TEST(HashtableKeyContextTest, HashtableEqAPInt) {
|
|
// Hashtable equality doesn't assert on mismatched bit width, it includes the
|
|
// bit width in the comparison.
|
|
llvm::APInt one_64(/*numBits=*/64, /*val=*/1);
|
|
llvm::APInt two_64(/*numBits=*/64, /*val=*/2);
|
|
llvm::APInt one_128(/*numBits=*/128, /*val=*/1);
|
|
llvm::APInt two_128(/*numBits=*/128, /*val=*/2);
|
|
|
|
EXPECT_TRUE(HashtableEq(one_64, one_64));
|
|
EXPECT_FALSE(HashtableEq(one_64, one_128));
|
|
EXPECT_TRUE(HashtableEq(two_128, two_128));
|
|
EXPECT_FALSE(HashtableEq(two_64, two_128));
|
|
EXPECT_FALSE(HashtableEq(one_64, two_64));
|
|
EXPECT_FALSE(HashtableEq(one_64, two_128));
|
|
EXPECT_FALSE(HashtableEq(one_128, two_128));
|
|
EXPECT_FALSE(HashtableEq(one_128, two_64));
|
|
}
|
|
|
|
TEST(HashtableKeyContextTest, HashtableEqAPFloat) {
|
|
// Hashtable equality for `APFloat` uses a bitwise comparison. This
|
|
// differentiates between various things that would otherwise not make sense:
|
|
// - Different floating point semantics
|
|
// - `-0.0` and `0.0`
|
|
//
|
|
// It also allows NaNs to be compared meaningfully.
|
|
llvm::APFloat zero_float =
|
|
llvm::APFloat::getZero(llvm::APFloat::IEEEsingle());
|
|
llvm::APFloat neg_zero_float =
|
|
llvm::APFloat::getZero(llvm::APFloat::IEEEsingle(), /*Negative=*/true);
|
|
llvm::APFloat zero_double =
|
|
llvm::APFloat::getZero(llvm::APFloat::IEEEdouble());
|
|
llvm::APFloat zero_bfloat = llvm::APFloat::getZero(llvm::APFloat::BFloat());
|
|
llvm::APFloat one_float = llvm::APFloat::getOne(llvm::APFloat::IEEEsingle());
|
|
llvm::APFloat inf_float = llvm::APFloat::getInf(llvm::APFloat::IEEEsingle());
|
|
llvm::APFloat nan_0_float = llvm::APFloat::getNaN(
|
|
llvm::APFloat::IEEEsingle(), /*Negative=*/false, /*payload=*/0);
|
|
llvm::APFloat nan_42_float = llvm::APFloat::getNaN(
|
|
llvm::APFloat::IEEEsingle(), /*Negative=*/false, /*payload=*/42);
|
|
|
|
// Boring cases.
|
|
EXPECT_TRUE(HashtableEq(zero_float, zero_float));
|
|
EXPECT_FALSE(HashtableEq(zero_float, one_float));
|
|
EXPECT_TRUE(HashtableEq(inf_float, inf_float));
|
|
EXPECT_FALSE(HashtableEq(inf_float, one_float));
|
|
|
|
// Confirm a case where we expect `==` to work but produce a different result.
|
|
ASSERT_TRUE(zero_float == neg_zero_float);
|
|
EXPECT_FALSE(HashtableEq(zero_float, neg_zero_float));
|
|
|
|
// Now work through less reasonable things outside of a hashtable such as
|
|
// mixing semantics and NaNs.
|
|
EXPECT_FALSE(HashtableEq(zero_float, zero_double));
|
|
EXPECT_FALSE(HashtableEq(zero_float, zero_bfloat));
|
|
EXPECT_FALSE(HashtableEq(zero_float, nan_0_float));
|
|
EXPECT_FALSE(HashtableEq(zero_float, nan_42_float));
|
|
EXPECT_FALSE(HashtableEq(nan_0_float, nan_42_float));
|
|
}
|
|
|
|
struct CustomHash {
|
|
int x;
|
|
|
|
friend auto CarbonHashValue(const CustomHash& value) -> HashCode {
|
|
return HashValue(value.x + 42);
|
|
}
|
|
};
|
|
|
|
TEST(HashtableKeyContextTest, DefaultKeyContext) {
|
|
// Make sure the default context dispatches appropriately, including for
|
|
// interesting types. We don't cover all the cases here and use the direct
|
|
// tests of `HashtableEq` for that.
|
|
DefaultKeyContext context;
|
|
|
|
EXPECT_FALSE(context.KeyEq(1234, 5678));
|
|
EXPECT_TRUE(context.KeyEq(5678, 5678));
|
|
EXPECT_TRUE(context.KeyEq(DefaultEq{0, 0}, DefaultEq{0, 0}));
|
|
EXPECT_FALSE(context.KeyEq(DefaultEq{1, 2}, DefaultEq{3, 4}));
|
|
EXPECT_TRUE(context.KeyEq(CustomEq{0, 0}, CustomEq{0, 0}));
|
|
EXPECT_FALSE(context.KeyEq(CustomEq{1, 2}, CustomEq{3, 4}));
|
|
EXPECT_TRUE(context.KeyEq(CustomExtEq{0, 0}, CustomExtEq{0, 0}));
|
|
EXPECT_FALSE(context.KeyEq(CustomExtEq{1, 2}, CustomExtEq{3, 4}));
|
|
|
|
llvm::APInt one_64(/*numBits=*/64, /*val=*/1);
|
|
llvm::APInt one_128(/*numBits=*/128, /*val=*/1);
|
|
EXPECT_TRUE(HashtableEq(one_64, one_64));
|
|
EXPECT_FALSE(HashtableEq(one_64, one_128));
|
|
|
|
llvm::APFloat zero_float =
|
|
llvm::APFloat::getZero(llvm::APFloat::IEEEsingle());
|
|
llvm::APFloat neg_zero_float =
|
|
llvm::APFloat::getZero(llvm::APFloat::IEEEsingle(), /*Negative=*/true);
|
|
EXPECT_TRUE(HashtableEq(zero_float, zero_float));
|
|
EXPECT_FALSE(HashtableEq(zero_float, neg_zero_float));
|
|
|
|
// Also check hash dispatching.
|
|
EXPECT_THAT(context.HashKey(42), Eq(HashValue(42)));
|
|
EXPECT_THAT(context.HashKey(CustomHash{.x = 1234}),
|
|
Eq(HashValue(CustomHash{.x = 1234})));
|
|
EXPECT_THAT(context.HashKey(one_64), Eq(HashValue(one_64)));
|
|
EXPECT_THAT(context.HashKey(one_128), Eq(HashValue(one_128)));
|
|
EXPECT_THAT(context.HashKey(one_64), Ne(context.HashKey(one_128)));
|
|
EXPECT_THAT(context.HashKey(zero_float), Eq(HashValue(zero_float)));
|
|
EXPECT_THAT(context.HashKey(neg_zero_float), Eq(HashValue(neg_zero_float)));
|
|
EXPECT_THAT(context.HashKey(zero_float), Ne(context.HashKey(neg_zero_float)));
|
|
}
|
|
|
|
struct TestTranslatingKeyContext
|
|
: TranslatingKeyContext<TestTranslatingKeyContext> {
|
|
auto TranslateKey(int index) const -> const llvm::APInt& {
|
|
return array[index];
|
|
}
|
|
|
|
llvm::ArrayRef<llvm::APInt> array;
|
|
};
|
|
|
|
TEST(HashtableKeyContextTest, TranslatingKeyContext) {
|
|
llvm::APInt one_64(/*numBits=*/64, /*val=*/1);
|
|
llvm::APInt two_64(/*numBits=*/64, /*val=*/2);
|
|
llvm::APInt one_128(/*numBits=*/128, /*val=*/1);
|
|
llvm::APInt two_128(/*numBits=*/128, /*val=*/2);
|
|
// An array of values, including some duplicates.
|
|
llvm::SmallVector<llvm::APInt> values = {one_64, two_64, one_128,
|
|
two_128, one_64, one_64};
|
|
|
|
TestTranslatingKeyContext context = {.array = values};
|
|
|
|
EXPECT_THAT(context.HashKey(0), Eq(HashValue(one_64)));
|
|
EXPECT_THAT(context.HashKey(1), Eq(HashValue(two_64)));
|
|
EXPECT_THAT(context.HashKey(2), Eq(HashValue(one_128)));
|
|
EXPECT_THAT(context.HashKey(3), Eq(HashValue(two_128)));
|
|
EXPECT_THAT(context.HashKey(4), Eq(HashValue(one_64)));
|
|
EXPECT_THAT(context.HashKey(5), Eq(HashValue(one_64)));
|
|
|
|
EXPECT_TRUE(context.KeyEq(one_64, 0));
|
|
EXPECT_TRUE(context.KeyEq(one_64, 4));
|
|
EXPECT_TRUE(context.KeyEq(one_64, 5));
|
|
EXPECT_TRUE(context.KeyEq(0, one_64));
|
|
EXPECT_TRUE(context.KeyEq(0, 0));
|
|
EXPECT_TRUE(context.KeyEq(0, 4));
|
|
EXPECT_TRUE(context.KeyEq(4, 5));
|
|
EXPECT_FALSE(context.KeyEq(one_64, 1));
|
|
EXPECT_FALSE(context.KeyEq(one_64, 2));
|
|
EXPECT_FALSE(context.KeyEq(one_64, 3));
|
|
EXPECT_FALSE(context.KeyEq(1, one_64));
|
|
EXPECT_FALSE(context.KeyEq(2, one_64));
|
|
EXPECT_FALSE(context.KeyEq(3, one_64));
|
|
EXPECT_FALSE(context.KeyEq(0, 1));
|
|
EXPECT_FALSE(context.KeyEq(0, 2));
|
|
EXPECT_FALSE(context.KeyEq(4, 3));
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace Carbon
|