memgraph/tests/benchmark/query/profile.cpp
János Benjamin Antal 537855a0b2
Fix usages of constexpr (#367)
* Fix usages of constexpr
2022-03-31 13:52:43 +02:00

684 lines
18 KiB
C++

// Copyright 2022 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
// License, and you may not use this file except in compliance with the Business Source License.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.
#include <memory>
#include <vector>
#include <benchmark/benchmark.h>
#include "query/context.hpp"
#include "utils/tsc.hpp"
/*
* We're benchmarking three different implementations of the profiling
* machinery:
*
* 1. Using `std::unordered_map` to associate every logical operator with its
* profiling stats.
*
* 2. Using a tree of profiling stats whose structure corresponds to the logical
* operators that were executed (no explicit association exists between a
* logical operator and its profiling stats).
*
* 3. The same as 2. but storing the nodes of the tree within a contiguous block
* of storage (pool).
*/
namespace {
//////////////////////////////////////////////////////////////////////////////
//
// NAryTree
/*
* A utility to help us imitate the traversal of a plan (and therefore the
* profiling statistics as well).
*/
struct NAryTree {
NAryTree(int64_t id) : id(id) {}
int64_t id;
std::vector<std::unique_ptr<NAryTree>> children;
};
std::unique_ptr<NAryTree> LiftTreeHelper(const std::vector<int64_t> &flattened, size_t *index) {
int64_t id = flattened[(*index)++];
size_t num_children = flattened[(*index)++];
auto node = std::make_unique<NAryTree>(id);
if (num_children == 0) {
return node;
}
for (int64_t i = 0; i < num_children; ++i) {
node->children.emplace_back(LiftTreeHelper(flattened, index));
}
return node;
}
/*
* A utility that produces instances of `NAryTree` given its "flattened"
* (serialized) form. A node (without its children) is serialized as a sequence
* of integers: `<node-id> <node-num-children>`. A tree is serialized by
* serializing its root node and then recursively (depth-first) serializing its
* subtrees (children).
*
* As an example, here's the general form of the produced serialization,
* starting from some root node: `<root-id> <root-num-children>
* <root-child-1-id> <root-child-1-num-children> <root-child-1-child-1-id>
* <root-child-1-child-1-num-children> ... <root-child-2-id>
* <root-child-2-num-children> ...`.
*/
std::unique_ptr<NAryTree> LiftTree(const std::vector<int64_t> &flattened) {
size_t index = 0;
return LiftTreeHelper(flattened, &index);
}
//////////////////////////////////////////////////////////////////////////////
///
/// Map variant
namespace map {
//////////////////////////////////////////////////////////////////////////////
//
// ProfilingStats
struct ProfilingStats {
// We're starting from -1 to facilitate unconditional incrementing within
// `Pull` (which makes the code of the `Pull` easier to follow). The last
// increment (when `Pull` returns `false`) will be superfluous so compensate
// for it here.
int64_t actual_hits{-1};
unsigned long long elapsed_time{0};
};
//////////////////////////////////////////////////////////////////////////////
//
// Context
struct EvaluationContext {
bool is_profile_query{true};
std::unordered_map<std::uintptr_t, ProfilingStats> operator_stats;
};
class Context {
public:
Context() = default;
Context(const Context &) = delete;
Context &operator=(const Context &) = delete;
Context(Context &&) = default;
Context &operator=(Context &&) = default;
EvaluationContext evaluation_context_;
};
//////////////////////////////////////////////////////////////////////////////
//
// ScopedProfile
struct ScopedProfile {
ScopedProfile(std::uintptr_t key, Context *context) : context(context) {
if (context->evaluation_context_.is_profile_query) {
stats = &context->evaluation_context_.operator_stats[key];
start_time = memgraph::utils::ReadTSC();
stats->actual_hits++;
}
}
~ScopedProfile() {
if (context->evaluation_context_.is_profile_query) {
stats->elapsed_time += memgraph::utils::ReadTSC() - start_time;
}
}
Context *context;
ProfilingStats *stats;
unsigned long long start_time;
};
//////////////////////////////////////////////////////////////////////////////
//
// Profile
void ProfileHelper(const NAryTree *tree, Context *context) {
ScopedProfile profile(reinterpret_cast<std::uintptr_t>(tree), context);
for (auto &child : tree->children) {
ProfileHelper(child.get(), context);
}
}
void Profile(const std::unique_ptr<NAryTree> &tree) {
Context context;
context.evaluation_context_.is_profile_query = true;
ProfileHelper(tree.get(), &context);
}
} // namespace map
//////////////////////////////////////////////////////////////////////////////
///
/// Tree variant
namespace tree {
//////////////////////////////////////////////////////////////////////////////
///
/// ProfilingStats
struct ProfilingStats {
ProfilingStats() : ProfilingStats(-1, 0) {}
ProfilingStats(int64_t actual_hits, unsigned long long elapsed_time)
: actual_hits(actual_hits), elapsed_time(elapsed_time), key(reinterpret_cast<std::uintptr_t>(nullptr)) {
// The logical operator with the most children has at most 3 children (but
// most of the logical operators have just 1 child)
children.reserve(3);
}
// We're starting from -1 to facilitate unconditional incrementing within
// `Pull` (which makes the code of the `Pull` easier to follow). The last
// increment (when `Pull` returns `false`) will be superfluous so compensate
// for it here.
int64_t actual_hits;
unsigned long long elapsed_time;
std::uintptr_t key;
std::vector<ProfilingStats> children;
};
//////////////////////////////////////////////////////////////////////////////
///
/// Context
struct EvaluationContext {
bool is_profile_query{true};
ProfilingStats stats;
ProfilingStats *stats_root{nullptr};
};
class Context {
public:
Context() = default;
Context(const Context &) = delete;
Context &operator=(const Context &) = delete;
Context(Context &&) = default;
Context &operator=(Context &&) = default;
EvaluationContext evaluation_context_;
};
//////////////////////////////////////////////////////////////////////////////
///
/// ScopedProfile
struct ScopedProfile {
ScopedProfile(std::uintptr_t key, Context *context) : context(context) {
if (context->evaluation_context_.is_profile_query) {
root = context->evaluation_context_.stats_root;
// Are we the root logical operator?
if (!root) {
stats = &context->evaluation_context_.stats;
} else {
stats = nullptr;
// Was this logical operator already hit on one of the previous pulls?
for (size_t i = 0; i < root->children.size(); ++i) {
if (root->children[i].key == key) {
stats = &root->children[i];
break;
}
}
if (!stats) {
root->children.emplace_back();
stats = &root->children.back();
}
}
context->evaluation_context_.stats_root = stats;
stats->actual_hits++;
stats->key = key;
start_time = memgraph::utils::ReadTSC();
}
}
~ScopedProfile() {
if (context->evaluation_context_.is_profile_query) {
stats->elapsed_time += memgraph::utils::ReadTSC() - start_time;
// Restore the old root ("pop")
context->evaluation_context_.stats_root = root;
}
}
Context *context;
ProfilingStats *root, *stats;
unsigned long long start_time;
};
//////////////////////////////////////////////////////////////////////////////
//
// Profile
void ProfileHelper(const NAryTree *tree, Context *context) {
ScopedProfile profile(reinterpret_cast<std::uintptr_t>(tree), context);
for (auto &child : tree->children) {
ProfileHelper(child.get(), context);
}
}
void Profile(const std::unique_ptr<NAryTree> &tree) {
Context context;
context.evaluation_context_.is_profile_query = true;
ProfileHelper(tree.get(), &context);
}
} // namespace tree
//////////////////////////////////////////////////////////////////////////////
///
/// Tree + No reserve variant
namespace tree_no_reserve {
//////////////////////////////////////////////////////////////////////////////
///
/// ProfilingStats
struct ProfilingStats {
// We're starting from -1 to facilitate unconditional incrementing within
// `Pull` (which makes the code of the `Pull` easier to follow). The last
// increment (when `Pull` returns `false`) will be superfluous so compensate
// for it here.
int64_t actual_hits{-1};
unsigned long long elapsed_time{0};
std::uintptr_t key{reinterpret_cast<std::uintptr_t>(nullptr)};
std::vector<ProfilingStats> children;
};
//////////////////////////////////////////////////////////////////////////////
///
/// Context
struct EvaluationContext {
bool is_profile_query{true};
ProfilingStats stats;
ProfilingStats *stats_root{nullptr};
};
class Context {
public:
Context() = default;
Context(const Context &) = delete;
Context &operator=(const Context &) = delete;
Context(Context &&) = default;
Context &operator=(Context &&) = default;
EvaluationContext evaluation_context_;
};
//////////////////////////////////////////////////////////////////////////////
///
/// ScopedProfile
struct ScopedProfile {
ScopedProfile(std::uintptr_t key, Context *context) : context(context) {
if (context->evaluation_context_.is_profile_query) {
root = context->evaluation_context_.stats_root;
// Are we the root logical operator?
if (!root) {
stats = &context->evaluation_context_.stats;
} else {
stats = nullptr;
// Was this logical operator already hit on one of the previous pulls?
for (size_t i = 0; i < root->children.size(); ++i) {
if (root->children[i].key == key) {
stats = &root->children[i];
break;
}
}
if (!stats) {
root->children.emplace_back();
stats = &root->children.back();
}
}
context->evaluation_context_.stats_root = stats;
stats->actual_hits++;
stats->key = key;
start_time = memgraph::utils::ReadTSC();
}
}
~ScopedProfile() {
if (context->evaluation_context_.is_profile_query) {
stats->elapsed_time += memgraph::utils::ReadTSC() - start_time;
// Restore the old root ("pop")
context->evaluation_context_.stats_root = root;
}
}
Context *context;
ProfilingStats *root, *stats;
unsigned long long start_time;
};
//////////////////////////////////////////////////////////////////////////////
//
// Profile
void ProfileHelper(const NAryTree *tree, Context *context) {
ScopedProfile profile(reinterpret_cast<std::uintptr_t>(tree), context);
for (auto &child : tree->children) {
ProfileHelper(child.get(), context);
}
}
void Profile(const std::unique_ptr<NAryTree> &tree) {
Context context;
context.evaluation_context_.is_profile_query = true;
ProfileHelper(tree.get(), &context);
}
} // namespace tree_no_reserve
//////////////////////////////////////////////////////////////////////////////
///
/// Tree + Storage variant
namespace tree_storage {
//////////////////////////////////////////////////////////////////////////////
//
// ProfilingStats
inline constexpr size_t kMaxProfilingStatsChildren = 3;
struct ProfilingStatsStorage;
struct ProfilingStats {
ProfilingStats();
ProfilingStats(int64_t actual_hits, unsigned long long elapsed_time);
size_t EnsureChild(const ProfilingStatsStorage &storage, std::uintptr_t key);
// We're starting from -1 to facilitate unconditional incrementing within
// `Pull` (which makes the code of the `Pull` easier to follow). The last
// increment (when `Pull` returns `false`) will be superfluous so compensate
// for it here.
int64_t actual_hits;
unsigned long long elapsed_time;
std::uintptr_t key;
std::array<int64_t, kMaxProfilingStatsChildren> children;
};
//////////////////////////////////////////////////////////////////////////////
///
/// ProfilingStatsStorage
struct ProfilingStatsStorage {
int64_t Create();
bool Empty();
ProfilingStats &Root();
const ProfilingStats &Root() const;
ProfilingStats *at(int64_t index);
const ProfilingStats *at(int64_t index) const;
ProfilingStats *operator[](int64_t index);
const ProfilingStats *operator[](int64_t index) const;
std::vector<ProfilingStats> storage;
};
int64_t ProfilingStatsStorage::Create() {
storage.emplace_back();
return storage.size() - 1;
}
bool ProfilingStatsStorage::Empty() { return storage.empty(); }
ProfilingStats &ProfilingStatsStorage::Root() {
MG_ASSERT(!storage.empty());
return storage[0];
}
const ProfilingStats &ProfilingStatsStorage::Root() const {
MG_ASSERT(!storage.empty());
return storage[0];
}
ProfilingStats *ProfilingStatsStorage::at(int64_t index) {
MG_ASSERT(0 <= index);
MG_ASSERT(index < storage.size());
return &storage[index];
}
const ProfilingStats *ProfilingStatsStorage::at(int64_t index) const {
MG_ASSERT(0 <= index);
MG_ASSERT(index < storage.size());
return &storage[index];
}
ProfilingStats *ProfilingStatsStorage::operator[](int64_t index) { return at(index); }
const ProfilingStats *ProfilingStatsStorage::operator[](int64_t index) const { return at(index); }
//////////////////////////////////////////////////////////////////////////////
//
// ProfilingStats
ProfilingStats::ProfilingStats() : ProfilingStats(-1, 0) {}
ProfilingStats::ProfilingStats(int64_t actual_hits, unsigned long long elapsed_time)
: actual_hits(actual_hits), elapsed_time(elapsed_time), key(reinterpret_cast<std::uintptr_t>(nullptr)), children{} {
for (auto &child : children) {
child = -1;
}
}
size_t ProfilingStats::EnsureChild(const ProfilingStatsStorage &storage, std::uintptr_t key) {
size_t size = children.size();
size_t slot = size;
for (size_t i = 0; i < size; ++i) {
if (children[i] == -1 || storage[children[i]]->key == key) {
slot = i;
break;
}
}
// Either we found an instance of `ProfilingStats` that was creater earlier
// and corresponds to the logical operator `op` or we're seeing this
// operator for the first time and have found a free slot for its
// `ProfilingStats` instance.
MG_ASSERT(slot < size);
return slot;
}
//////////////////////////////////////////////////////////////////////////////
///
/// Context
struct EvaluationContext {
bool is_profile_query{true};
ProfilingStatsStorage stats_storage;
int64_t stats_root{-1};
};
class Context {
public:
Context() = default;
Context(const Context &) = delete;
Context &operator=(const Context &) = delete;
Context(Context &&) = default;
Context &operator=(Context &&) = default;
EvaluationContext evaluation_context_;
};
//////////////////////////////////////////////////////////////////////////////
//
// ScopedProfile
struct ScopedProfile {
ScopedProfile(std::uintptr_t key, Context *context) : context(context), old_root_id(-1), stats_id(-1) {
if (context->evaluation_context_.is_profile_query) {
stats_id = EnsureStats(context->evaluation_context_.stats_root, key);
auto &storage = context->evaluation_context_.stats_storage;
ProfilingStats *stats = storage[stats_id];
MG_ASSERT(stats);
stats->actual_hits++;
start_time = memgraph::utils::ReadTSC();
stats->key = key;
// Set the new root ("push")
old_root_id = context->evaluation_context_.stats_root;
context->evaluation_context_.stats_root = stats_id;
}
}
int64_t EnsureStats(int64_t root_id, std::uintptr_t key) {
auto &storage = context->evaluation_context_.stats_storage;
if (root_id == -1) {
if (storage.Empty()) {
return storage.Create();
}
return 0;
}
ProfilingStats *root = storage[root_id];
size_t slot = root->EnsureChild(storage, key);
int64_t stats_id = root->children[slot];
if (stats_id == -1) {
stats_id = storage.Create();
// `Create` might have invalidated the `root` pointer so index `storage`
// explicitly again
storage[root_id]->children[slot] = stats_id;
}
MG_ASSERT(stats_id >= 0);
return stats_id;
}
~ScopedProfile() {
auto &storage = context->evaluation_context_.stats_storage;
ProfilingStats *stats = storage[stats_id];
MG_ASSERT(stats);
if (context->evaluation_context_.is_profile_query) {
stats->elapsed_time += memgraph::utils::ReadTSC() - start_time;
// Restore the old root ("pop")
context->evaluation_context_.stats_root = old_root_id;
}
}
Context *context;
int64_t old_root_id, stats_id;
unsigned long long start_time;
};
//////////////////////////////////////////////////////////////////////////////
//
// Profile
void ProfileHelper(const NAryTree *tree, Context *context) {
ScopedProfile profile(reinterpret_cast<std::uintptr_t>(tree), context);
for (auto &child : tree->children) {
ProfileHelper(child.get(), context);
}
}
void Profile(const std::unique_ptr<NAryTree> &tree) {
Context context;
context.evaluation_context_.is_profile_query = true;
ProfileHelper(tree.get(), &context);
}
} // namespace tree_storage
} // namespace
std::vector<std::unique_ptr<NAryTree>> GetTrees() {
std::vector<std::vector<int64_t>> flat_trees = {
// {-1, 1, -2, 1, -3, 1, -4, 0},
// {-1, 1, -2, 1, -3, 3, -4, 1, -5, 1, -6, 0, -7, 1, -8, 1, -6, 0, -6, 0},
{-1, 1, -2, 2, -3, 2, -4, 2, -5, 1, -6, 1, -7, 0, -7, 0, -7, 0, -3, 1, -4, 2, -5, 1, -6, 1, -7, 0, -7, 0}};
std::vector<std::unique_ptr<NAryTree>> trees;
for (auto &flattened : flat_trees) {
trees.emplace_back(LiftTree(flattened));
}
return trees;
}
void BM_PlanProfileMap(benchmark::State &state) {
auto trees = GetTrees();
while (state.KeepRunning()) {
for (auto &tree : trees) {
map::Profile(tree);
}
}
}
void BM_PlanProfileTree(benchmark::State &state) {
auto trees = GetTrees();
while (state.KeepRunning()) {
for (auto &tree : trees) {
tree::Profile(tree);
}
}
}
void BM_PlanProfileTreeNoReserve(benchmark::State &state) {
auto trees = GetTrees();
while (state.KeepRunning()) {
for (auto &tree : trees) {
tree_no_reserve::Profile(tree);
}
}
}
void BM_PlanProfileTreeAndStorage(benchmark::State &state) {
auto trees = GetTrees();
while (state.KeepRunning()) {
for (auto &tree : trees) {
tree_storage::Profile(tree);
}
}
}
BENCHMARK(BM_PlanProfileMap);
BENCHMARK(BM_PlanProfileTree);
BENCHMARK(BM_PlanProfileTreeNoReserve);
BENCHMARK(BM_PlanProfileTreeAndStorage);
BENCHMARK_MAIN();