diff --git a/premake5.lua b/premake5.lua index c043f46..2f8aece 100644 --- a/premake5.lua +++ b/premake5.lua @@ -44,50 +44,6 @@ project "gtest" filter "system:macosx" pic "on" -project "benchmark" - location "build" - kind "StaticLib" - language "C++" - cppdialect "C++20" - staticruntime "on" - - targetdir ("bin/" .. outputdir .. "/output") - objdir ("bin/" .. outputdir .. "/intermediate") - - files { - "thirdparty/benchmark/src/**.cc", - } - - removefiles { - "thirdparty/benchmark/src/benchmark_main.cc", - } - - includedirs { - "thirdparty/benchmark/include", - "thirdparty/benchmark/src", - } - - defines { "BENCHMARK_STATIC_DEFINE", "HAVE_STD_REGEX" } - - filter "configurations:debug" - runtime "Debug" - symbols "on" - optimize "off" - - filter "configurations:release" - runtime "Release" - symbols "on" - optimize "Speed" - - filter "system:windows" - systemversion "latest" - - filter "system:linux" - pic "on" - - filter "system:macosx" - pic "on" - project "gtest_main" location "build" kind "StaticLib" @@ -120,6 +76,51 @@ project "gtest_main" filter "system:windows" systemversion "latest" +project "benchmark" + location "build" + kind "StaticLib" + language "C++" + cppdialect "C++20" + staticruntime "on" + + targetdir ("bin/" .. outputdir .. "/output") + objdir ("bin/" .. outputdir .. "/intermediate") + + files { + "thirdparty/benchmark/src/**.cc", + } + + removefiles { + "thirdparty/benchmark/src/benchmark_main.cc", + } + + includedirs { + "thirdparty/benchmark/include", + "thirdparty/benchmark/src", + } + + defines { "BENCHMARK_STATIC_DEFINE", "HAVE_STD_REGEX" } + + filter "configurations:debug" + runtime "Debug" + symbols "on" + optimize "off" + + filter "configurations:release" + defines { "NDEBUG" } + runtime "Release" + symbols "on" + optimize "Speed" + + filter "system:windows" + systemversion "latest" + + filter "system:linux" + pic "on" + + filter "system:macosx" + pic "on" + project "tenvex" location "build" kind "None" @@ -176,6 +177,7 @@ project "tenvex_tests" includedirs { "src/tenvex", + "src/naive", "thirdparty/gtest/googletest/include", } @@ -222,6 +224,7 @@ project "tenvex_bench" } includedirs { + "src/naive", "src/tenvex", "thirdparty/benchmark/include", } @@ -240,7 +243,7 @@ project "tenvex_bench" runtime "Release" symbols "on" optimize "Speed" - defines { "TENVEX_RELEASE" } + defines { "TENVEX_RELEASE", "NDEBUG" } filter "system:windows" systemversion "latest" diff --git a/src/benchmarks/bench_naive.cpp b/src/benchmarks/bench_naive.cpp new file mode 100644 index 0000000..d1adbac --- /dev/null +++ b/src/benchmarks/bench_naive.cpp @@ -0,0 +1,92 @@ +#include "naive_vec4.h" + +#include + +#include +#include + +using namespace naive; + +// File-local; mirrors make_vecs from bench_vec4.cpp with the same seed, +// so the naive and tenvex benchmarks run on identical input data. +static std::vector make_vecs(int n, unsigned seed) +{ + std::mt19937 rng(seed); + std::uniform_real_distribution d(-1.0f, 1.0f); + std::vector v; + v.reserve(n); + for (int i = 0; i < n; ++i) + v.push_back(vec4 { d(rng), d(rng), d(rng), 0.0f }); + return v; +} + +static void BM_Naive_Dot3_Latency(benchmark::State& state) +{ + vec4 a { 1.0f, 2.0f, 3.0f, 0.0f }; + vec4 b { 0.5f, 0.5f, 0.5f, 0.0f }; + float acc = 0.0f; + for (auto _ : state) + { + vec4 bb = b + vec4 { acc, 0.0f, 0.0f, 0.0f }; + float d = dot3(a, bb); + acc = d * 1e-7f; + benchmark::DoNotOptimize(acc); + } +} +BENCHMARK(BM_Naive_Dot3_Latency); + +static void BM_Naive_Norm3_Latency(benchmark::State& state) +{ + vec4 v { 1.0f, 2.0f, 3.0f, 1.0f }; + for (auto _ : state) + { + v = norm3(v) + vec4 { 1e-3f, 2e-3f, 3e-3f, 0.0f }; + benchmark::DoNotOptimize(v); + } +} +BENCHMARK(BM_Naive_Norm3_Latency); + +static void BM_Naive_Dot3_Throughput(benchmark::State& state) +{ + const auto a = make_vecs(1024, 1); + const auto b = make_vecs(1024, 2); + std::size_t i = 0; + for (auto _ : state) + { + float d = dot3(a[i], b[i]); + benchmark::DoNotOptimize(d); + i = (i + 1) & 1023; + } +} +BENCHMARK(BM_Naive_Dot3_Throughput); + +static void BM_Naive_Compound(benchmark::State& state) +{ + const auto va = make_vecs(1024, 1); + const auto vb = make_vecs(1024, 2); + const auto vc = make_vecs(1024, 3); + std::size_t i = 0; + for (auto _ : state) + { + vec4 r = norm3(va[i] + vb[i] * 2.0f) * dot3(vb[i], vc[i]) + vc[i] * 3.0f; + benchmark::DoNotOptimize(r); + i = (i + 1) & 1023; + } +} +BENCHMARK(BM_Naive_Compound); + +static void BM_Naive_FacingSameWay(benchmark::State& state) +{ + vec4 fa = norm3(vec4 { 1.0f, 0.0f, 0.2f, 0.0f }); + vec4 fb = vec4 { 0.9f, 0.1f, 0.1f, 0.0f }; + float jitter = 0.0f; + for (auto _ : state) + { + vec4 f = norm3(fb + vec4 { jitter, 0.0f, 0.0f, 0.0f }); + float c = dot3(fa, f); + bool same = c > 0.95f; + jitter = c * 1e-6f; + benchmark::DoNotOptimize(same); + } +} +BENCHMARK(BM_Naive_FacingSameWay); diff --git a/src/naive/naive_vec4.h b/src/naive/naive_vec4.h new file mode 100644 index 0000000..910efd8 --- /dev/null +++ b/src/naive/naive_vec4.h @@ -0,0 +1,92 @@ +#pragma once + +#include + +namespace naive +{ + +struct vec4 +{ + vec4() noexcept = default; + + vec4(float x, float y, float z, float w = 0.0f) noexcept + : _x(x), _y(y), _z(z), _w(w) + { + } + + [[nodiscard]] float x() const noexcept { return _x; } + [[nodiscard]] float y() const noexcept { return _y; } + [[nodiscard]] float z() const noexcept { return _z; } + [[nodiscard]] float w() const noexcept { return _w; } + +private: + float _x, _y, _z, _w; +}; + +[[nodiscard]] inline vec4 operator+(vec4 a, vec4 b) noexcept +{ + return { a.x() + b.x(), a.y() + b.y(), a.z() + b.z(), a.w() + b.w() }; +} + +[[nodiscard]] inline vec4 operator-(vec4 a, vec4 b) noexcept +{ + return { a.x() - b.x(), a.y() - b.y(), a.z() - b.z(), a.w() - b.w() }; +} + +[[nodiscard]] inline vec4 operator*(vec4 v, float s) noexcept +{ + return { v.x() * s, v.y() * s, v.z() * s, v.w() * s }; +} + +[[nodiscard]] inline vec4 operator*(float s, vec4 v) noexcept +{ + return v * s; +} + +[[nodiscard]] inline vec4 operator/(vec4 v, float s) noexcept +{ + const float inv = 1.0f / s; // one division, then multiplies + return { v.x() * inv, v.y() * inv, v.z() * inv, v.w() * inv }; +} + +[[nodiscard]] inline float dot3(vec4 a, vec4 b) noexcept +{ + return a.x() * b.x() + a.y() * b.y() + a.z() * b.z(); +} + +[[nodiscard]] inline vec4 cross3(vec4 a, vec4 b) noexcept +{ + return { + a.y() * b.z() - a.z() * b.y(), + a.z() * b.x() - a.x() * b.z(), + a.x() * b.y() - a.y() * b.x(), + 0.0f + }; +} + +[[nodiscard]] inline float magnitude3(vec4 v) noexcept +{ + return std::sqrt(dot3(v, v)); +} + +[[nodiscard]] inline vec4 norm3(vec4 v) noexcept +{ + const float inv = 1.0f / magnitude3(v); // normalize xyz, keep w + return { v.x() * inv, v.y() * inv, v.z() * inv, v.w() }; +} + +inline bool operator==(vec4 a, vec4 b) noexcept +{ + return a.x() == b.x() && a.y() == b.y() + && a.z() == b.z() && a.w() == b.w(); +} + +inline bool approx_eq(vec4 a, vec4 b, float eps = 1e-6f) noexcept +{ + return std::fabs(a.x() - b.x()) <= eps + && std::fabs(a.y() - b.y()) <= eps + && std::fabs(a.z() - b.z()) <= eps + && std::fabs(a.w() - b.w()) <= eps; +} + +} \ No newline at end of file diff --git a/src/tests/naive_tests.cpp b/src/tests/naive_tests.cpp new file mode 100644 index 0000000..d145f25 --- /dev/null +++ b/src/tests/naive_tests.cpp @@ -0,0 +1,256 @@ +#include "naive_vec4.h" + +#include "gtest/gtest.h" + +#include + +namespace naive +{ + +TEST(naive_vec4, add) +{ + vec4 a = { 1.0f, 2.0f, 3.0f, 4.0f }; + vec4 b = { 4.0f, 3.0f, 2.0f, 1.0f }; + vec4 check = { 5.0f, 5.0f, 5.0f, 5.0f }; + vec4 result = a + b; + + EXPECT_TRUE(approx_eq(check, result)); +} + +TEST(naive_vec4, mixed_type_add) +{ + vec4 a = { 1.0f, 2.0f, 3.0f, 0.0f }; + vec4 b = { 1.0f, 1.0f, 1.0f, 0.0f }; + vec4 c = { 2.0f, 0.0f, 0.0f, 0.0f }; + vec4 check = { 4.0f, 3.0f, 4.0f, 0.0f }; + + vec4 result = a + b + c; + EXPECT_TRUE(approx_eq(check, result)); +} + +TEST(naive_vec4, sub) +{ + vec4 a = { 5.0f, 5.0f, 5.0f, 5.0f }; + vec4 b = { 4.0f, 3.0f, 2.0f, 1.0f }; + vec4 check = { 1.0f, 2.0f, 3.0f, 4.0f }; + vec4 result = a - b; + + EXPECT_TRUE(approx_eq(check, result)); +} + +TEST(naive_vec4, mult_a) +{ + vec4 a = { 1.0f, 2.0f, 0.0f, 0.0f }; + float b = 3.0f; + vec4 check = { 3.0f, 6.0f, 0.0f, 0.0f }; + vec4 result = a * b; + + EXPECT_TRUE(approx_eq(check, result)); +} + +TEST(naive_vec4, mult_b) +{ + float a = 2.0f; + vec4 b = { 1.0f, 0.0f, 2.0f, 0.0f }; + vec4 check = { 2.0f, 0.0f, 4.0f, 0.0f }; + vec4 result = a * b; + + EXPECT_TRUE(approx_eq(check, result)); +} + +TEST(naive_vec4, div) +{ + vec4 a = { 3.0f, 6.0f, 0.0f, 0.0f }; + float b = 3.0f; + vec4 check = { 1.0f, 2.0f, 0.0f, 0.0f }; + vec4 result = a / b; + EXPECT_TRUE(approx_eq(check, result)); +} + +TEST(naive_vec4, dot3) +{ + vec4 a = { 1.0f, 0.0f, 3.0f, 1.0f }; + vec4 b = { 1.0f, 2.0f, 3.0f, 4.0f }; + float check = 10.0f; + float result = dot3(a, b); + + EXPECT_FLOAT_EQ(check, result); +} + +TEST(naive_vec4, cross) +{ + vec4 a = { 1.0f, 0.0f, 0.0f, 0.0f }; + vec4 b = { 0.0f, 1.0f, 0.0f, 0.0f }; + vec4 check = { 0.0f, 0.0f, 1.0f, 0.0f }; + vec4 result = cross3(a, b); + + EXPECT_TRUE(approx_eq(check, result)); +} + +TEST(naive_vec4, norm_a) +{ + vec4 a = { 1.0f, 0.0f, 0.0f, 1.0f }; + vec4 check = { 1.0f, 0.0f, 0.0f, 1.0f }; + vec4 result = norm3(a); + + EXPECT_TRUE(approx_eq(check, result)); +} + +TEST(naive_vec4, norm_b) +{ + vec4 a = { 1.0f, 0.0f, 1.0f, 1.0f }; + vec4 check = { 0.707106f, 0.0f, 0.707106f, 1.0f }; + vec4 result = norm3(a); + + EXPECT_TRUE(approx_eq(check, result, 1e-3f)); +} + +TEST(naive_vec4, magnitude_a) +{ + vec4 a = { 1.0f, 0.0f, 0.0f, 0.0f }; + float check = 1.0f; + float result = magnitude3(a); + + EXPECT_FLOAT_EQ(check, result); +} + +TEST(naive_vec4, magnitude_b) +{ + vec4 a = { 1.0f, 0.0f, 1.0f, 0.0f }; + float check = 1.4142135f; + float result = magnitude3(a); + + EXPECT_FLOAT_EQ(check, result); +} + +TEST(naive_vec4, expression_a) +{ + vec4 a = { 1.0f, 1.0f, 1.0f, 1.0f }; + vec4 b = { 0.0f, 1.0f, 0.0f, 3.0f }; + vec4 c = { 1.15f, 0.0f, 11.0f, 0.0f }; + + vec4 check = { 3.45f, 0.0f, 33.0f, 0.0f }; + + vec4 result = dot3(a, b) * 3.0f * c; + + EXPECT_TRUE(approx_eq(check, result)); +} + +TEST(naive_vec4, expression_b) +{ + vec4 a = { 1.0f, 1.0f, 1.0f, 0.0f }; + vec4 b = { 0.0f, 1.0f, 0.0f, 0.0f }; + vec4 c = { 1.15f, 0.0f, 11.0f, 0.0f }; + + vec4 check = { 33.0f, 0.0f, -3.45f, 0.0f }; + + vec4 result = dot3(a, b) * 3.0f * cross3(b, c); + + EXPECT_TRUE(approx_eq(check, result)); +} + +TEST(naive_vec4, expression_c) +{ + vec4 a = { 1.0f, 2.0f, 3.0f, 0.0f }; + vec4 b = { 4.0f, 5.0f, 6.0f, 0.0f }; + vec4 c = { 1.0f, 0.0f, 1.0f, 0.0f }; + + vec4 check = { 7.24264050f, 5.65685463f, 10.0710678f, 0.0f }; + + vec4 tmp1 = norm3(a + b * 2.0f) * dot3(b, c); + vec4 tmp2 = c * 3.0f; + + vec4 result = tmp1 + tmp2; + + EXPECT_TRUE(approx_eq(check, result, 1e-3f)); +} + +TEST(naive_vec4, expression_d) +{ + vec4 a = { 1.0f, 0.0f, 0.0f, 0.0f }; + vec4 b = { 0.0f, 2.0f, 0.0f, 0.0f }; + vec4 c = { 0.0f, 0.0f, 3.0f, 0.0f }; + vec4 result = (a + b) * 2.0f + c - a; + vec4 check = { 1.0f, 4.0f, 3.0f, 0.0f }; + EXPECT_TRUE(approx_eq(check, result)); +} + +TEST(naive_vec4, expression_from_temporaries) +{ + vec4 a = { 1.0f, 2.0f, 3.0f, 0.0f }; + vec4 b = { 4.0f, 5.0f, 6.0f, 0.0f }; + vec4 check = { 2.0f, 4.0f, 6.0f, 0.0f }; + vec4 result = (a + b) + (a - b); + + EXPECT_TRUE(approx_eq(check, result)); +} + +TEST(naive_vec4, cross_anticommutative) +{ + vec4 a = { 1.0f, 0.0f, 0.0f, 0.0f }; + vec4 b = { 0.0f, 1.0f, 0.0f, 0.0f }; + + EXPECT_TRUE(approx_eq(cross3(a, b), cross3(b, a) * -1.0f)); +} + +TEST(naive_vec4, w_is_zero_with_nonzero_input_w) +{ + vec4 a = { 1.0f, 0.0f, 0.0f, 5.0f }; + vec4 b = { 0.0f, 1.0f, 0.0f, 7.0f }; + + vec4 r = cross3(a, b); + + EXPECT_FLOAT_EQ(r.w(), 0.0f); + EXPECT_FLOAT_EQ(r.z(), 1.0f); +} + +TEST(naive_vec4, preserves_nontrivial_w) +{ + vec4 a = { 0.0f, 3.0f, 0.0f, 9.0f }; + vec4 r = norm3(a); + + EXPECT_FLOAT_EQ(r.y(), 1.0f); + EXPECT_FLOAT_EQ(r.w(), 9.0f); +} + +TEST(naive_vec4, dot_commutative) +{ + vec4 a = { 1.0f, 2.0f, 3.0f, 0.0f }; + vec4 b = { 4.0f, 5.0f, 6.0f, 0.0f }; + + EXPECT_FLOAT_EQ(dot3(a, b), dot3(b, a)); +} + +TEST(naive_vec4, norm_magnitude) +{ + vec4 a = { 3.0f, 1.0f, 4.0f, 0.0f }; + + EXPECT_NEAR(magnitude3(norm3(a)), 1.0f, 1e-6f); +} + +TEST(naive_vec4, zero_vector_is_nan) +{ + vec4 z = { 0.0f, 0.0f, 0.0f, 0.0f }; + vec4 r = norm3(z); + + EXPECT_TRUE(std::isnan(r.x())); +} + +TEST(naive_vec4, dot_perpendicular) +{ + vec4 a = { 1.0f, 0.0f, 0.0f, 0.0f }; + vec4 b = { 0.0f, 1.0f, 0.0f, 0.0f }; + + EXPECT_FLOAT_EQ(dot3(a, b), 0.0f); +} + +TEST(naive_vec4, cross_parallel) +{ + vec4 a = { 1.0f, 0.0f, 0.0f, 0.0f }; + vec4 b = { 2.0f, 0.0f, 0.0f, 0.0f }; + vec4 check = { 0.0f, 0.0f, 0.0f, 0.0f }; + + EXPECT_TRUE(approx_eq(cross3(a, b), check)); +} + +} \ No newline at end of file