| //===- ImmutableMergeBM.cpp - Benchmark ImmutableSet/Map merges -----------===// |
| // |
| // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
| // See https://llvm.org/LICENSE.txt for license information. |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| // |
| //===----------------------------------------------------------------------===// |
| // |
| // Compares the single-pass, structure-sharing merge (ImmutableSet::unionSets / |
| // ImmutableMap::mergeWith) against the per-element add loop it replaced, on the |
| // operand shapes that dominate dataflow joins: |
| // |
| // * Small - both operands tiny (constant-factor sensitive). |
| // * NearId - both large but differing by only a few keys (the fixpoint |
| // case, where most subtrees are shared). |
| // |
| // It covers a plain set, a scalar-valued map, and the map-of-sets shape used by |
| // the Clang lifetime analysis (ImmutableMap<Origin, ImmutableSet<Loan>>, whose |
| // join key-wise unions the inner loan sets). The point of these benchmarks is |
| // to confirm the bulk merge is not slower than the add loop for small or nearly |
| // identical inputs. Run before/after and compare (e.g. llvm/utils/compare.py). |
| // |
| //===----------------------------------------------------------------------===// |
| |
| #include "benchmark/benchmark.h" |
| #include "llvm/ADT/ImmutableMap.h" |
| #include "llvm/ADT/ImmutableSet.h" |
| #include <algorithm> |
| #include <memory> |
| #include <numeric> |
| #include <vector> |
| |
| using namespace llvm; |
| |
| namespace { |
| |
| using IntSet = |
| ImmutableSet<int, ImutContainerInfo<int>, /*Canonicalize=*/false>; |
| using IntMap = |
| ImmutableMap<int, int, ImutKeyValueInfo<int, int>, /*Canonicalize=*/false>; |
| // The lifetime analysis' OriginLoanMap shape: a map whose values are sets. |
| using SetMap = ImmutableMap<int, IntSet, ImutKeyValueInfo<int, IntSet>, |
| /*Canonicalize=*/false>; |
| |
| //===----------------------------------------------------------------------===// |
| // The per-element merges the bulk path replaced. |
| //===----------------------------------------------------------------------===// |
| |
| static IntSet setUnionAdd(IntSet::Factory &F, IntSet A, IntSet B) { |
| if (A.getRootWithoutRetain() == B.getRootWithoutRetain() || B.isEmpty()) |
| return A; |
| if (A.isEmpty()) |
| return B; |
| if (A.getHeight() < B.getHeight()) |
| std::swap(A, B); |
| for (int E : B) |
| A = F.add(A, E); |
| return A; |
| } |
| |
| static IntMap mapUnionAdd(IntMap::Factory &F, IntMap A, IntMap B) { |
| if (A.getRootWithoutRetain() == B.getRootWithoutRetain()) |
| return A; |
| if (A.getHeight() < B.getHeight()) |
| std::swap(A, B); |
| IntMap Res = A; |
| for (IntMap::iterator I = B.begin(), E = B.end(); I != E; ++I) { |
| const int *AV = A.lookup(I.getKey()); |
| Res = F.add(Res, I.getKey(), AV ? std::max(*AV, I.getData()) : I.getData()); |
| } |
| return Res; |
| } |
| |
| static SetMap setMapUnionAdd(SetMap::Factory &MF, IntSet::Factory &SF, SetMap A, |
| SetMap B) { |
| if (A.getRootWithoutRetain() == B.getRootWithoutRetain()) |
| return A; |
| if (A.getHeight() < B.getHeight()) |
| std::swap(A, B); |
| SetMap Res = A; |
| for (SetMap::iterator I = B.begin(), E = B.end(); I != E; ++I) { |
| const IntSet *AV = A.lookup(I.getKey()); |
| IntSet Merged = AV ? setUnionAdd(SF, *AV, I.getData()) : I.getData(); |
| Res = MF.add(Res, I.getKey(), Merged); |
| } |
| return Res; |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // The bulk merges (mirroring the caller-side short-circuit/swap the analyses |
| // perform around them, so both sides are compared on equal footing). |
| //===----------------------------------------------------------------------===// |
| |
| static IntSet setUnionNew(IntSet::Factory &F, IntSet A, IntSet B) { |
| return F.unionSets(A, B); |
| } |
| |
| static IntMap mapUnionNew(IntMap::Factory &F, IntMap A, IntMap B) { |
| if (A.getRootWithoutRetain() == B.getRootWithoutRetain()) |
| return A; |
| if (A.getHeight() < B.getHeight()) |
| std::swap(A, B); |
| auto Combine = [](const IntMap::value_type *AE, |
| const IntMap::value_type *BE) -> std::pair<int, int> { |
| int Key = AE ? AE->first : BE->first; |
| return {Key, std::max(AE ? AE->second : BE->second, |
| BE ? BE->second : AE->second)}; |
| }; |
| return F.mergeWith(A, B, Combine, /*KeepUnmatched=*/true); |
| } |
| |
| static SetMap setMapUnionNew(SetMap::Factory &MF, IntSet::Factory &SF, SetMap A, |
| SetMap B) { |
| if (A.getRootWithoutRetain() == B.getRootWithoutRetain()) |
| return A; |
| if (A.getHeight() < B.getHeight()) |
| std::swap(A, B); |
| auto Combine = [&SF](const SetMap::value_type *AE, |
| const SetMap::value_type *BE) -> std::pair<int, IntSet> { |
| int Key = AE ? AE->first : BE->first; |
| IntSet AS = AE ? AE->second : SF.getEmptySet(); |
| IntSet BS = BE ? BE->second : SF.getEmptySet(); |
| return {Key, SF.unionSets(AS, BS)}; |
| }; |
| return MF.mergeWith(A, B, Combine, /*KeepUnmatched=*/true); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // Key generators. Small: two tiny half-overlapping sets. NearId: two size-N |
| // sets sharing all but D keys (D unique to each side). |
| //===----------------------------------------------------------------------===// |
| |
| static void makeKeys(bool NearId, int N, int D, std::vector<int> &KA, |
| std::vector<int> &KB) { |
| KA.resize(N); |
| std::iota(KA.begin(), KA.end(), 0); |
| KB = KA; |
| if (NearId) { |
| // Replace the first D keys of B with fresh keys, so A and B share N-D keys |
| // and each has D unique ones. |
| for (int I = 0; I < D && I < N; ++I) |
| KB[I] = N + I; |
| } else { |
| // Half-overlap: shift B up by N/2. |
| for (int &V : KB) |
| V += N / 2; |
| } |
| } |
| |
| // Bound allocator growth: results accumulate in the factory, so periodically |
| // rebuild it (excluded from timing) rather than let it grow unboundedly. |
| static constexpr int ResetEvery = 4096; |
| |
| static IntSet buildSet(IntSet::Factory &F, const std::vector<int> &Keys) { |
| IntSet S = F.getEmptySet(); |
| for (int V : Keys) |
| S = F.add(S, V); |
| return S; |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // Benchmarks. |
| //===----------------------------------------------------------------------===// |
| |
| static void BM_Set(benchmark::State &State, bool UseNew, bool NearId, int N, |
| int D) { |
| std::vector<int> KA, KB; |
| makeKeys(NearId, N, D, KA, KB); |
| |
| std::unique_ptr<IntSet::Factory> F; |
| IntSet A{nullptr}, B{nullptr}; |
| auto reset = [&] { |
| A = IntSet(nullptr); |
| B = IntSet(nullptr); |
| F = std::make_unique<IntSet::Factory>(); |
| A = buildSet(*F, KA); |
| B = buildSet(*F, KB); |
| }; |
| reset(); |
| |
| int Since = 0; |
| for (auto _ : State) { |
| // Rebuild before creating this iteration's result, so no live result |
| // references the factory being torn down. |
| if (++Since == ResetEvery) { |
| State.PauseTiming(); |
| reset(); |
| Since = 0; |
| State.ResumeTiming(); |
| } |
| IntSet U = UseNew ? setUnionNew(*F, A, B) : setUnionAdd(*F, A, B); |
| benchmark::DoNotOptimize(U.getRootWithoutRetain()); |
| } |
| } |
| |
| // Like BM_Set, but B is derived from A by adding D fresh keys, so B shares all |
| // of A's untouched subtrees by pointer -- the realistic "state after a small |
| // edit" shape that the pointer-equality skip in unionSets is meant to exploit. |
| static void BM_SetShared(benchmark::State &State, bool UseNew, int N, int D) { |
| std::vector<int> KA(N); |
| std::iota(KA.begin(), KA.end(), 0); |
| |
| std::unique_ptr<IntSet::Factory> F; |
| IntSet A{nullptr}, B{nullptr}; |
| auto reset = [&] { |
| A = IntSet(nullptr); |
| B = IntSet(nullptr); |
| F = std::make_unique<IntSet::Factory>(); |
| A = buildSet(*F, KA); |
| B = A; |
| for (int I = 0; I < D; ++I) |
| B = F->add(B, N + I); |
| }; |
| reset(); |
| |
| int Since = 0; |
| for (auto _ : State) { |
| if (++Since == ResetEvery) { |
| State.PauseTiming(); |
| reset(); |
| Since = 0; |
| State.ResumeTiming(); |
| } |
| IntSet U = UseNew ? setUnionNew(*F, A, B) : setUnionAdd(*F, A, B); |
| benchmark::DoNotOptimize(U.getRootWithoutRetain()); |
| } |
| } |
| |
| static void BM_Map(benchmark::State &State, bool UseNew, bool NearId, int N, |
| int D) { |
| std::vector<int> KA, KB; |
| makeKeys(NearId, N, D, KA, KB); |
| |
| std::unique_ptr<IntMap::Factory> F; |
| IntMap A{nullptr}, B{nullptr}; |
| auto reset = [&] { |
| A = IntMap(nullptr); |
| B = IntMap(nullptr); |
| F = std::make_unique<IntMap::Factory>(); |
| IntMap MA = F->getEmptyMap(), MB = F->getEmptyMap(); |
| for (int V : KA) |
| MA = F->add(MA, V, V); |
| for (int V : KB) |
| MB = F->add(MB, V, V); |
| A = MA; |
| B = MB; |
| }; |
| reset(); |
| |
| int Since = 0; |
| for (auto _ : State) { |
| if (++Since == ResetEvery) { |
| State.PauseTiming(); |
| reset(); |
| Since = 0; |
| State.ResumeTiming(); |
| } |
| IntMap U = UseNew ? mapUnionNew(*F, A, B) : mapUnionAdd(*F, A, B); |
| benchmark::DoNotOptimize(U.getRootWithoutRetain()); |
| } |
| } |
| |
| // Map<int, Set<int>>: each key maps to a small set; the merge key-wise unions |
| // the value sets (the lifetime OriginLoanMap join). |
| static void BM_MapOfSets(benchmark::State &State, bool UseNew, bool NearId, |
| int N, int D, int SetSize) { |
| std::vector<int> KA, KB; |
| makeKeys(NearId, N, D, KA, KB); |
| |
| std::unique_ptr<SetMap::Factory> MF; |
| std::unique_ptr<IntSet::Factory> SF; |
| SetMap A{nullptr}, B{nullptr}; |
| auto valueSet = [&](int Key) { |
| // A deterministic small set derived from the key. |
| IntSet S = SF->getEmptySet(); |
| for (int I = 0; I < SetSize; ++I) |
| S = SF->add(S, Key * 31 + I); |
| return S; |
| }; |
| auto reset = [&] { |
| A = SetMap(nullptr); |
| B = SetMap(nullptr); |
| MF = std::make_unique<SetMap::Factory>(); |
| SF = std::make_unique<IntSet::Factory>(); |
| SetMap MA = MF->getEmptyMap(), MB = MF->getEmptyMap(); |
| for (int V : KA) |
| MA = MF->add(MA, V, valueSet(V)); |
| for (int V : KB) |
| MB = MF->add(MB, V, valueSet(V)); |
| A = MA; |
| B = MB; |
| }; |
| reset(); |
| |
| int Since = 0; |
| for (auto _ : State) { |
| if (++Since == ResetEvery) { |
| State.PauseTiming(); |
| reset(); |
| Since = 0; |
| State.ResumeTiming(); |
| } |
| SetMap U = UseNew ? setMapUnionNew(*MF, *SF, A, B) |
| : setMapUnionAdd(*MF, *SF, A, B); |
| benchmark::DoNotOptimize(U.getRootWithoutRetain()); |
| } |
| } |
| |
| } // namespace |
| |
| // Small: both operands tiny. NearId: size N, differ by D keys each side. |
| BENCHMARK_CAPTURE(BM_Set, "Set/Small/Add", false, false, 8, 0); |
| BENCHMARK_CAPTURE(BM_Set, "Set/Small/Union", true, false, 8, 0); |
| BENCHMARK_CAPTURE(BM_Set, "Set/Small16/Add", false, false, 16, 0); |
| BENCHMARK_CAPTURE(BM_Set, "Set/Small16/Union", true, false, 16, 0); |
| // Small and nearly identical (share all but one key each side) -- the common |
| // dataflow live-set shape, and the case most exposed to split overhead. |
| BENCHMARK_CAPTURE(BM_Set, "Set/SmallNearId8/Add", false, true, 8, 1); |
| BENCHMARK_CAPTURE(BM_Set, "Set/SmallNearId8/Union", true, true, 8, 1); |
| // Very small sets, delta 2 (and a couple of neighbours) to see if the Δ1 |
| // regression persists at tiny sizes once the diff is at least 2. |
| BENCHMARK_CAPTURE(BM_Set, "Set/N8D2/Add", false, true, 8, 2); |
| BENCHMARK_CAPTURE(BM_Set, "Set/N8D2/Union", true, true, 8, 2); |
| BENCHMARK_CAPTURE(BM_Set, "Set/N16D2/Add", false, true, 16, 2); |
| BENCHMARK_CAPTURE(BM_Set, "Set/N16D2/Union", true, true, 16, 2); |
| BENCHMARK_CAPTURE(BM_Set, "Set/N32D2/Add", false, true, 32, 2); |
| BENCHMARK_CAPTURE(BM_Set, "Set/N32D2/Union", true, true, 32, 2); |
| BENCHMARK_CAPTURE(BM_Set, "Set/SmallNearId64/Add", false, true, 64, 1); |
| BENCHMARK_CAPTURE(BM_Set, "Set/SmallNearId64/Union", true, true, 64, 1); |
| // Delta sweep at N=64 (where the near-identical regression peaked): how does |
| // the gap behave as the two sets differ by more keys? |
| BENCHMARK_CAPTURE(BM_Set, "Set/N64D2/Add", false, true, 64, 2); |
| BENCHMARK_CAPTURE(BM_Set, "Set/N64D2/Union", true, true, 64, 2); |
| BENCHMARK_CAPTURE(BM_Set, "Set/N64D4/Add", false, true, 64, 4); |
| BENCHMARK_CAPTURE(BM_Set, "Set/N64D4/Union", true, true, 64, 4); |
| BENCHMARK_CAPTURE(BM_Set, "Set/N64D8/Add", false, true, 64, 8); |
| BENCHMARK_CAPTURE(BM_Set, "Set/N64D8/Union", true, true, 64, 8); |
| BENCHMARK_CAPTURE(BM_Set, "Set/N64D16/Add", false, true, 64, 16); |
| BENCHMARK_CAPTURE(BM_Set, "Set/N64D16/Union", true, true, 64, 16); |
| BENCHMARK_CAPTURE(BM_Set, "Set/N64D32/Add", false, true, 64, 32); |
| BENCHMARK_CAPTURE(BM_Set, "Set/N64D32/Union", true, true, 64, 32); |
| // Δ1 but B derived from A (shares subtrees by pointer): the case the |
| // pointer-equality skip targets. Compare against the independent-build |
| // Set/*NearId* numbers above. |
| BENCHMARK_CAPTURE(BM_SetShared, "SetShared/N8D1/Add", false, 8, 1); |
| BENCHMARK_CAPTURE(BM_SetShared, "SetShared/N8D1/Union", true, 8, 1); |
| BENCHMARK_CAPTURE(BM_SetShared, "SetShared/N64D1/Add", false, 64, 1); |
| BENCHMARK_CAPTURE(BM_SetShared, "SetShared/N64D1/Union", true, 64, 1); |
| BENCHMARK_CAPTURE(BM_SetShared, "SetShared/N1024D1/Add", false, 1024, 1); |
| BENCHMARK_CAPTURE(BM_SetShared, "SetShared/N1024D1/Union", true, 1024, 1); |
| BENCHMARK_CAPTURE(BM_Set, "Set/NearId128/Add", false, true, 128, 1); |
| BENCHMARK_CAPTURE(BM_Set, "Set/NearId128/Union", true, true, 128, 1); |
| BENCHMARK_CAPTURE(BM_Set, "Set/NearId256/Add", false, true, 256, 1); |
| BENCHMARK_CAPTURE(BM_Set, "Set/NearId256/Union", true, true, 256, 1); |
| BENCHMARK_CAPTURE(BM_Set, "Set/NearId512/Add", false, true, 512, 1); |
| BENCHMARK_CAPTURE(BM_Set, "Set/NearId512/Union", true, true, 512, 1); |
| BENCHMARK_CAPTURE(BM_Set, "Set/NearId/Add", false, true, 1024, 1); |
| BENCHMARK_CAPTURE(BM_Set, "Set/NearId/Union", true, true, 1024, 1); |
| |
| BENCHMARK_CAPTURE(BM_Map, "Map/Small/Add", false, false, 8, 0); |
| BENCHMARK_CAPTURE(BM_Map, "Map/Small/Union", true, false, 8, 0); |
| BENCHMARK_CAPTURE(BM_Map, "Map/SmallNearId8/Add", false, true, 8, 1); |
| BENCHMARK_CAPTURE(BM_Map, "Map/SmallNearId8/Union", true, true, 8, 1); |
| BENCHMARK_CAPTURE(BM_Map, "Map/SmallNearId64/Add", false, true, 64, 1); |
| BENCHMARK_CAPTURE(BM_Map, "Map/SmallNearId64/Union", true, true, 64, 1); |
| BENCHMARK_CAPTURE(BM_Map, "Map/NearId/Add", false, true, 1024, 1); |
| BENCHMARK_CAPTURE(BM_Map, "Map/NearId/Union", true, true, 1024, 1); |
| |
| BENCHMARK_CAPTURE(BM_MapOfSets, "MapOfSets/Small/Add", false, false, 8, 0, 4); |
| BENCHMARK_CAPTURE(BM_MapOfSets, "MapOfSets/Small/Union", true, false, 8, 0, 4); |
| BENCHMARK_CAPTURE(BM_MapOfSets, "MapOfSets/SmallNearId8/Add", false, true, 8, 1, |
| 4); |
| BENCHMARK_CAPTURE(BM_MapOfSets, "MapOfSets/SmallNearId8/Union", true, true, 8, |
| 1, 4); |
| BENCHMARK_CAPTURE(BM_MapOfSets, "MapOfSets/NearId/Add", false, true, 256, 1, 8); |
| BENCHMARK_CAPTURE(BM_MapOfSets, "MapOfSets/NearId/Union", true, true, 256, 1, |
| 8); |
| |
| BENCHMARK_MAIN(); |