blob: a638f3e1e453a8c839e7cb9016a19e0086f79573 [file] [edit]
//===- 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();