Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ endif()

list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake")

option(BUILD_BENCHMARKS "Build micro-benchmarks (nanobench)" OFF)

include(CTest)
include(Compiler)
include(Modules)
Expand All @@ -30,3 +32,24 @@ add_subdirectory(Engine/cpp)

# Samples (C++)
add_subdirectory(Samples/cpp)

# Aggregate benchmark targets: `benchmarks` builds them all, `run-benchmarks`
# builds and runs each in turn.
if(BUILD_BENCHMARKS)
get_property(DRACO_BENCH_TARGETS GLOBAL PROPERTY DRACO_BENCH_TARGETS)
if(DRACO_BENCH_TARGETS)
add_custom_target(benchmarks)
add_dependencies(benchmarks ${DRACO_BENCH_TARGETS})

set(BENCH_RUN_CMDS "")
foreach(BENCH_TARGET IN LISTS DRACO_BENCH_TARGETS)
list(APPEND BENCH_RUN_CMDS COMMAND $<TARGET_FILE:${BENCH_TARGET}>)
endforeach()
add_custom_target(run-benchmarks
${BENCH_RUN_CMDS}
DEPENDS ${DRACO_BENCH_TARGETS}
USES_TERMINAL
VERBATIM
)
endif()
endif()
65 changes: 65 additions & 0 deletions Docs/Benchmarking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Benchmarking

As a major goal for draconic is performance, we need a way to benchmark
everything. Therefore, I have opted into adding nanobench in to help with this.

Simply add a `FILENAME.bench.cpp` next to the FILENAME.cpp that you are
testing

Here is a sample of how the benchmark runs:
```cpp
#define ANKERL_NANOBENCH_IMPLEMENT
#include <nanobench.h>

import core.math;

int main() {
ankerl::nanobench::Bench bench;
bench.title("Vector4::dot").relative(true);

// runtime value:
float x = 1.0f;
bench.run("dot", [&] {
float r = /* call the thing */;
ankerl::nanobench::doNotOptimizeAway(r);
});
}
```

It can be built with this flag: `-DBUILD_BENCHMARKS=ON`

This builds all of the *.bench.cpp:
`cmake --build build/release --target benchmarks`

This will build and run them all in a sequence:
`cmake --build build/release --target run-benchmarks`


## Optional configurations

I need to note: Defaults are sane.

```cpp
ankerl::nanobench::Bench b;

// More samples, higher confidence.
// If err% is too high, increase the value.
// NOTE: It will increase the time cost.
b.epochs(21); // default: 11

// Longer epochs provide steadier output.
// Edit this for fast operations (i.e. dot)
b.minEpochTime(std::chrono::milliseconds(10));

// Forces at least this amount of iterations per epoch.
// Use if you need more runs.
b.minEpochIterations(1'000'000);

// Fixed, exact iterations here.
// Use this for reproducibility. This is just for determinism.
b.epochIterations(100'000);

// Throwaway runs before the timer starts.
// Use this if it touches memory / has branches
b.warmup(100);
```
143 changes: 143 additions & 0 deletions Engine/cpp/Runtime/Core/Math/Vector4.bench.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Vector4.bench.cpp

// Currently tests just the Vector4::dot, simd vs scalar.
// This tests throughput, dependency-chain latency, compare/count.
// This is mostly just for an example of how to use it until we
// expand on it a fair amount more.

#define ANKERL_NANOBENCH_IMPLEMENT
#include <nanobench.h>

#include <cstddef>
#include <cstdio>
#include <random>
#include <vector>

import core.math;

namespace {

constexpr auto scalar_dot = [](const draco::math::Vector4& x, const draco::math::Vector4& y) {
return x.x * y.x + x.y * y.y + x.z * y.z + x.w * y.w;
};

constexpr auto simd_dot = [](const draco::math::Vector4& x, const draco::math::Vector4& y) {
return draco::math::dot(x, y);
};

// Build config output:
void print_metadata() {
#if defined(__clang__)
std::printf("# compiler : clang %d.%d.%d\n", __clang_major__, __clang_minor__, __clang_patchlevel__);
#elif defined(__GNUC__)
std::printf("# compiler : gcc %d.%d.%d\n", __GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__);
#elif defined(_MSC_VER)
std::printf("# compiler : msvc %d\n", _MSC_VER);
#endif

#if defined(__FAST_MATH__)
std::printf("# fast-math: on\n");
#else
std::printf("# fast-math: off\n");
#endif

std::printf("# sizeof(Vector4) = %zu\n", sizeof(draco::math::Vector4));
std::printf("# alignof(Vector4) = %zu\n", alignof(draco::math::Vector4));
}

// 4 accumulators over the first n elements (n % 4 == 0) so nothing serializes.
void throughput4(ankerl::nanobench::Bench& bench, const char* name, auto dot, const std::vector<draco::math::Vector4>& a, const std::vector<draco::math::Vector4>& b, std::size_t n) {
bench.run(name, [&, n] {
draco::f32 acc0 = 0.0f, acc1 = 0.0f, acc2 = 0.0f, acc3 = 0.0f;
for (std::size_t i = 0; i < n; i += 4) {
acc0 += dot(a[i + 0], b[i + 0]);
acc1 += dot(a[i + 1], b[i + 1]);
acc2 += dot(a[i + 2], b[i + 2]);
acc3 += dot(a[i + 3], b[i + 3]);
}
ankerl::nanobench::doNotOptimizeAway(acc0 + acc1 + acc2 + acc3);
});
}

void benchmark_dot() {
constexpr std::size_t COUNT = 1024;
static_assert(COUNT % 4 == 0, "throughput loop is unrolled by 4");

std::mt19937 rng(0xDEADBABE); // ebin
std::uniform_real_distribution<draco::f32> dist(-1.0f, 1.0f);

const auto rnd = [&] { return dist(rng); };

std::vector<draco::math::Vector4> a(COUNT), b(COUNT);

for (std::size_t i = 0; i < COUNT; ++i) {
a[i] = { rnd(), rnd(), rnd(), rnd() };
b[i] = { rnd(), rnd(), rnd(), rnd() };
}

const auto configure = [&](ankerl::nanobench::Bench& bench, const char* title) {
bench
.title(title)
.relative(true)
.unit("dot")
.batch(COUNT)
.epochs(20) // tigher median.
.epochIterations(10000); // force reproduciblility across runs.
};

// Throughput: independent dots, peak case.
ankerl::nanobench::Bench throughputBench;
configure(throughputBench, "dot throughput");
throughput4(throughputBench, "scalar", scalar_dot, a, b, COUNT);
throughput4(throughputBench, "simd", simd_dot, a, b, COUNT);

// Latency: each dot result feeds the next dot input, so calls don't overlap.
// Includes scalar-to-vector feedback overhead.
ankerl::nanobench::Bench chainBench;
configure(chainBench, "dot latency chain");

const auto latencyChain = [&](const char* name, auto dot) {
chainBench.run(name, [&] {
draco::math::Vector4 v = a[0];
draco::f32 s = 0.25f;

for (std::size_t i = 0; i < COUNT; ++i) {
v = { s, s, s, s };
s = dot(v, b[i]);
}

ankerl::nanobench::doNotOptimizeAway(s);
ankerl::nanobench::doNotOptimizeAway(v);
});
};

latencyChain("scalar", scalar_dot);
latencyChain("simd", simd_dot);

// Compare/count: dot feeds a threshold test (culling/visibility). May
// compile branchlessly, so it is compare/count cost, not branch prediction.
ankerl::nanobench::Bench compareBench;
configure(compareBench, "dot compare/count");

const auto compareCount = [&](const char* name, auto dot) {
compareBench.run(name, [&] {
int hits = 0;

for (std::size_t i = 0; i < COUNT; ++i) {
if (dot(a[i], b[i]) > 0.0f) ++hits;
}

ankerl::nanobench::doNotOptimizeAway(hits);
});
};

compareCount("scalar", scalar_dot);
compareCount("simd", simd_dot);
}

} // namespace

int main() {
print_metadata();
benchmark_dot();
}
1 change: 1 addition & 0 deletions Engine/cpp/Runtime/Core/Math/Vector4.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module;

#include <cmath>
#include <format>
#include "Runtime/Platform/intrinsics.h"

export module core.math.types:vector4;
export import :common;
Expand Down
11 changes: 11 additions & 0 deletions Engine/cpp/Runtime/Platform/intrinsics.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// platform/intrinsics.h

#pragma once

#include "Runtime/Platform/simd.h"

#if ARCH_X64
#include <immintrin.h>
#elif ARCH_ARM64
#include <arm_neon.h>
#endif
Loading
Loading