diff --git a/.bazelrc b/.bazelrc new file mode 100644 index 0000000..c6e89de --- /dev/null +++ b/.bazelrc @@ -0,0 +1,22 @@ +# Performance measurements should use optimized builds by default. +build -c opt + +# Keep CI logs readable and include failed test output. +common:ci --announce_rc +common:ci --color=yes +common:ci --curses=no +test:ci --test_output=errors + +# Instrument test binaries and their C/C++ dependencies for memory errors. +build:asan -c dbg +build:asan --copt=-fsanitize=address +build:asan --copt=-fno-omit-frame-pointer +build:asan --linkopt=-fsanitize=address +test:asan --test_env=ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 + +# Instrument test binaries and their C/C++ dependencies for undefined behavior. +build:ubsan -c dbg +build:ubsan --copt=-fsanitize=undefined +build:ubsan --copt=-fno-omit-frame-pointer +build:ubsan --linkopt=-fsanitize=undefined +test:ubsan --test_env=UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 diff --git a/src/.bazelversion b/.bazelversion similarity index 100% rename from src/.bazelversion rename to .bazelversion diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..26973f0 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,13 @@ +--- +# Apply the clang-tidy checks that enforce the Google C++ Style Guide. +Checks: 'google-*' + +# Format any clang-tidy fix-its with the corresponding clang-format style. +FormatStyle: Google + +# Analyze project headers without reporting diagnostics from external modules. +HeaderFilterRegex: '(^|.*/)(lib|benchmarks)/.*' + +# Treat all configured diagnostics as errors by default. +WarningsAsErrors: '*' +... diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..29788b8 --- /dev/null +++ b/.clangd @@ -0,0 +1,2 @@ +CompileFlags: + CompilationDatabase: . diff --git a/.github/workflows/bazel.yaml b/.github/workflows/bazel.yaml index 50f2d12..b76563d 100644 --- a/.github/workflows/bazel.yaml +++ b/.github/workflows/bazel.yaml @@ -1,4 +1,4 @@ -name: Bazel +name: CI on: pull_request: @@ -7,15 +7,41 @@ on: push: branches: - main + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: - test: - runs-on: ubuntu-latest + lint: + name: Lint + runs-on: ubuntu-24.04 + timeout-minutes: 15 + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: bazel-contrib/setup-bazel@c5acdfb288317d0b5c0bbd7a396a3dc868bb0f86 # 0.19.0 + with: + bazelisk-cache: true + disk-cache: ${{ github.workflow }}-${{ github.job }} + repository-cache: true + cache-save: ${{ github.event_name == 'push' }} + + - name: Install clang-tidy + run: sudo apt-get update && sudo apt-get install --yes clang-tidy + + - name: Lint C++ sources + run: sh tools/run-clang-tidy.sh - defaults: - run: - # MODULE.bazel and .bazelversion live in src/ - working-directory: src + build-and-test: + name: Build and test + runs-on: ubuntu-24.04 + timeout-minutes: 20 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -23,19 +49,41 @@ jobs: - uses: bazel-contrib/setup-bazel@c5acdfb288317d0b5c0bbd7a396a3dc868bb0f86 # 0.19.0 with: bazelisk-cache: true - disk-cache: ${{ github.workflow }} + disk-cache: ${{ github.workflow }}-${{ github.job }} repository-cache: true - # Pull requests only restore caches, pushes to main refresh them cache-save: ${{ github.event_name == 'push' }} - bazelrc: | - common --announce_rc - common --color=yes - - name: Bazel version - run: bazel version + - name: Build + run: bazel build --config=ci //... + + - name: Test + run: bazel test --config=ci //... + + sanitizers: + name: ${{ matrix.name }} + runs-on: ubuntu-24.04 + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + include: + - name: AddressSanitizer + config: asan + - name: UndefinedBehaviorSanitizer + config: ubsan + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: bazel-contrib/setup-bazel@c5acdfb288317d0b5c0bbd7a396a3dc868bb0f86 # 0.19.0 + with: + bazelisk-cache: true + disk-cache: ${{ github.workflow }}-${{ github.job }}-${{ matrix.config }} + repository-cache: true + cache-save: ${{ github.event_name == 'push' }} - name: Build - run: bazel build //... + run: bazel build --config=ci --config=${{ matrix.config }} //... - name: Test - run: bazel test //... --test_output=errors + run: bazel test --config=ci --config=${{ matrix.config }} //... diff --git a/.gitignore b/.gitignore index 7db0bd7..523150b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ *~ bazel-* *MODULE.bazel.lock +compile_commands.json /.ijwb/ /.aswb/ /.clwb/ diff --git a/MODULE.bazel b/MODULE.bazel new file mode 100644 index 0000000..f826d4d --- /dev/null +++ b/MODULE.bazel @@ -0,0 +1,19 @@ +# C/C++ rules for the reusable frame-processing libraries and executables. +bazel_dep(name = "rules_cc", version = "0.2.26") + +# Test-only dependency; production libraries in lib/ do not depend on it. +bazel_dep(name = "googletest", version = "1.18.0.bcr.1", dev_dependency = True) + +# Benchmark executables depend on this package, never libraries in lib/. +bazel_dep(name = "google_benchmark", version = "1.9.5") + +# Generates compile_commands.json for clangd and clang-tidy. This is a +# development-only tool and is not part of production or benchmark targets. +# The pinned revision is the upstream Bazel 9 compatibility patch (PR #281), +# pending its merge into the canonical repository. +bazel_dep(name = "hedron_compile_commands", dev_dependency = True) +git_override( + module_name = "hedron_compile_commands", + remote = "https://github.com/ultimateanu/bazel-compile-commands-extractor.git", + commit = "2dc498f24587eccc2752f8a892da004d9baa8318", +) diff --git a/README.md b/README.md index 99b5b71..0ec06d4 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,96 @@ # memcopy-bench -DevTools 2026 Research Project. Violet Team -# Setup +A C/C++ research framework for evaluating data movement and frame-processing +techniques in Telecom L2 scenarios. The initial focus is PDCP and GTP-U frame +processing on x86, while reusable library code avoids unnecessary +architecture-specific assumptions. -We use [Bazel](https://bazel.build/install) to build, run and test our project. -The Bazel version is pinned in `src/.bazelversion`, use -[Bazelisk](https://github.com/bazelbuild/bazelisk) to get the right one. +## Layout + +- `lib/` — reusable frame, memory, transport, and scenario implementations. + Libraries do not depend on benchmarking or reporting infrastructure. + `lib/test/` is reserved for shared test-only support; component unit tests + remain colocated with their implementations. +- `benchmarks/` — Google Benchmark executables. `memory/` holds operation + microbenchmarks; `scenarios/` will hold complete workflow benchmarks. +- `tests/integration/` — reserved for cross-component integration tests when + they are needed. Unit tests are colocated with the library code they verify. +- `tools/` — experiment automation and analysis utilities. +- `results/` — raw benchmark output and experiment metadata (kept separate + from analysis). +- `reports/` — research findings and comparative analysis. + +## Prerequisites + +Install [Bazelisk](https://github.com/bazelbuild/bazelisk). It selects the +Bazel version pinned in `.bazelversion`. Commands are run from the repository +root. Optimized mode is enabled by default in `.bazelrc`. + +## C++ editor tooling + +Generate Bazel-aware compile commands for `clangd` and `clang-tidy` after +changing C++ sources, targets, dependencies, or compile settings: + +```bash +bazel run @hedron_compile_commands//:refresh_all +``` + +The generated root-level `compile_commands.json` is intentionally ignored by +Git. Restart the editor language server after refreshing it if it does not +reload the database automatically. + +## Build, lint, and test -All commands below are run from `src/`: ```bash -cd src/ +bazel build //... +bazel test //... ``` -Optimized build mode (`-c opt`) is enabled by default in `src/.bazelrc`. +Run the same C++ lint check used by CI (requires `clang-tidy` on `PATH`): -**Build:** ```bash -bazel build //lib:memcpy_bench # build benchmark lib only -bazel build //main:main # build main binary and library +sh tools/run-clang-tidy.sh ``` -**Test:** +Run tests with the supported sanitizers: + +```bash +bazel test --config=asan //... --test_output=errors +bazel test --config=ubsan //... --test_output=errors +``` + +For example, the initial reusable algorithm and its colocated test are: + ```bash -bazel test //test:memcpy_bench_test +bazel build //lib/memory:memcpy +bazel test //lib/memory:memcpy_test ``` -**Run benchmarks:** +## Run a benchmark + ```bash -bazel run //main:main +bazel run //benchmarks/memory:memcpy_benchmark -# Google Benchmark flags are passed after `--` -bazel run //main:main -- --benchmark_filter='BM_Memcpy/4096' --benchmark_format=json +# Google Benchmark options follow `--`. +bazel run //benchmarks/memory:memcpy_benchmark -- \ + --benchmark_filter='BM_Memcpy/4096' \ + --benchmark_format=json \ + --benchmark_out=results/memcpy-4096.json ``` + +Store the matching experiment metadata (scenario, algorithm, input size, +compiler configuration, CPU information, and source revision) alongside raw +output in `results/`. Use `reports/` for the derived analysis. + +## Development workflow + +1. Add an algorithm or explicit frame-processing scenario under `lib/`. +2. Add colocated functional tests and run `bazel test`. +3. Add or extend the corresponding executable under `benchmarks/`. +4. Run controlled experiments and record raw measurements plus metadata in + `results/`. +5. Analyze results in `reports/`. + +A scenario describes *what* happens to a frame; algorithms describe *how* an +individual operation is performed. Keep those concerns separate so algorithms +can be compared across independent research cases. diff --git a/benchmarks/memory/.clang-tidy b/benchmarks/memory/.clang-tidy new file mode 100644 index 0000000..60e0249 --- /dev/null +++ b/benchmarks/memory/.clang-tidy @@ -0,0 +1,3 @@ +# Google Benchmark range-for loops require an intentionally unused binding. +InheritParentConfig: true +Checks: '-clang-analyzer-deadcode.DeadStores' diff --git a/benchmarks/memory/BUILD.bazel b/benchmarks/memory/BUILD.bazel new file mode 100644 index 0000000..8d0c940 --- /dev/null +++ b/benchmarks/memory/BUILD.bazel @@ -0,0 +1,10 @@ +load("@rules_cc//cc:cc_binary.bzl", "cc_binary") + +cc_binary( + name = "memcpy_benchmark", + srcs = ["memcpy_benchmark.cpp"], + deps = [ + "//lib/memory:memcpy", + "@google_benchmark//:benchmark_main", + ], +) diff --git a/benchmarks/memory/memcpy_benchmark.cpp b/benchmarks/memory/memcpy_benchmark.cpp new file mode 100644 index 0000000..49d976e --- /dev/null +++ b/benchmarks/memory/memcpy_benchmark.cpp @@ -0,0 +1,41 @@ +#include "lib/memory/memcpy.h" + +#include +#include +#include + +#include + +namespace { + +std::vector MakeInput(std::size_t size) { + std::vector input(size); + std::uint32_t state = 0xC0FFEEu; + for (std::byte& value : input) { + state = state * 1664525u + 1013904223u; + value = static_cast(state >> 24); + } + return input; +} + +void BM_Memcpy(benchmark::State& state) { + const auto size = static_cast(state.range(0)); + const std::vector source = MakeInput(size); + std::vector destination(size); + + for (auto _ : state) { + memcopy_bench::memory::CopyWithMemcpy( + destination.data(), source.data(), size); + benchmark::DoNotOptimize(destination.data()); + benchmark::ClobberMemory(); + } + + state.SetBytesProcessed(static_cast(state.iterations()) * + static_cast(size)); +} + +BENCHMARK(BM_Memcpy)->RangeMultiplier(4)->Range(64, 64 << 20); + +} // namespace + +BENCHMARK_MAIN(); diff --git a/lib/memory/BUILD.bazel b/lib/memory/BUILD.bazel new file mode 100644 index 0000000..df95dea --- /dev/null +++ b/lib/memory/BUILD.bazel @@ -0,0 +1,18 @@ +load("@rules_cc//cc:cc_library.bzl", "cc_library") +load("@rules_cc//cc:cc_test.bzl", "cc_test") + +cc_library( + name = "memcpy", + srcs = ["memcpy.cpp"], + hdrs = ["memcpy.h"], + visibility = ["//visibility:public"], +) + +cc_test( + name = "memcpy_test", + srcs = ["memcpy_test.cpp"], + deps = [ + ":memcpy", + "@googletest//:gtest_main", + ], +) diff --git a/lib/memory/memcpy.cpp b/lib/memory/memcpy.cpp new file mode 100644 index 0000000..24fe8e7 --- /dev/null +++ b/lib/memory/memcpy.cpp @@ -0,0 +1,11 @@ +#include "lib/memory/memcpy.h" + +#include + +namespace memcopy_bench::memory { + +void CopyWithMemcpy(void* destination, const void* source, std::size_t size) { + std::memcpy(destination, source, size); +} + +} // namespace memcopy_bench::memory diff --git a/lib/memory/memcpy.h b/lib/memory/memcpy.h new file mode 100644 index 0000000..5ecd2d0 --- /dev/null +++ b/lib/memory/memcpy.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +namespace memcopy_bench::memory { + +// Copies exactly size bytes from source to destination using std::memcpy. +void CopyWithMemcpy(void* destination, const void* source, std::size_t size); + +} // namespace memcopy_bench::memory diff --git a/lib/memory/memcpy_test.cpp b/lib/memory/memcpy_test.cpp new file mode 100644 index 0000000..1dc6b78 --- /dev/null +++ b/lib/memory/memcpy_test.cpp @@ -0,0 +1,36 @@ +#include "lib/memory/memcpy.h" + +#include +#include + +#include "gtest/gtest.h" + +namespace memcopy_bench::memory { +namespace { + +TEST(CopyWithMemcpyTest, CopiesRequestedBytes) { + constexpr std::array source = { + std::byte{0x01}, std::byte{0x02}, std::byte{0x03}, std::byte{0x04}}; + std::array destination{}; + + CopyWithMemcpy(destination.data(), source.data(), source.size()); + + EXPECT_EQ(destination, source); +} + +TEST(CopyWithMemcpyTest, DoesNotWritePastRequestedSize) { + constexpr std::array source = { + std::byte{0x01}, std::byte{0x02}, std::byte{0x03}, std::byte{0x04}}; + std::array destination = { + std::byte{0xFF}, std::byte{0xFF}, std::byte{0xFF}, std::byte{0xFF}}; + + CopyWithMemcpy(destination.data(), source.data(), 2); + + EXPECT_EQ(destination[0], source[0]); + EXPECT_EQ(destination[1], source[1]); + EXPECT_EQ(destination[2], std::byte{0xFF}); + EXPECT_EQ(destination[3], std::byte{0xFF}); +} + +} // namespace +} // namespace memcopy_bench::memory diff --git a/lib/test/BUILD.bazel b/lib/test/BUILD.bazel new file mode 100644 index 0000000..3592bc7 --- /dev/null +++ b/lib/test/BUILD.bazel @@ -0,0 +1,4 @@ +# Shared test-only targets (fixtures, frame builders, and correctness helpers) +# for lib/ packages belong here. Component unit tests remain colocated with +# their implementations. +package(default_testonly = True) diff --git a/lib/test/README.md b/lib/test/README.md new file mode 100644 index 0000000..e0e33de --- /dev/null +++ b/lib/test/README.md @@ -0,0 +1,10 @@ +# Shared library test support + +This Bazel package is reserved for reusable, test-only support used by unit +tests across `lib/` components, such as deterministic frame builders, shared +correctness assertions, and test fixtures. + +Keep a component's unit test next to its implementation (for example, +`lib/memory/memcpy_test.cpp`). Add a target here only when the same test-only +code is genuinely needed by more than one component. Do not add Google +Benchmark dependencies or production implementation code to this package. diff --git a/reports/.gitkeep b/reports/.gitkeep new file mode 100644 index 0000000..120d7c7 --- /dev/null +++ b/reports/.gitkeep @@ -0,0 +1 @@ +# Store research findings and comparative analysis here. diff --git a/results/.gitkeep b/results/.gitkeep new file mode 100644 index 0000000..b56801f --- /dev/null +++ b/results/.gitkeep @@ -0,0 +1 @@ +# Store raw benchmark output and matching experiment metadata here. diff --git a/src/.bazelrc b/src/.bazelrc deleted file mode 100644 index f67d8fc..0000000 --- a/src/.bazelrc +++ /dev/null @@ -1 +0,0 @@ -build -c opt diff --git a/src/MODULE.bazel b/src/MODULE.bazel deleted file mode 100644 index 009a936..0000000 --- a/src/MODULE.bazel +++ /dev/null @@ -1,8 +0,0 @@ -# https://registry.bazel.build/modules/rules_cc -bazel_dep(name = "rules_cc", version = "0.2.26") - -# https://registry.bazel.build/modules/googletest -bazel_dep(name = "googletest", version = "1.18.0.bcr.1", dev_dependency = True) - -# https://registry.bazel.build/modules/google_benchmark -bazel_dep(name = "google_benchmark", version = "1.9.5") diff --git a/src/lib/BUILD b/src/lib/BUILD deleted file mode 100644 index 32723ad..0000000 --- a/src/lib/BUILD +++ /dev/null @@ -1,14 +0,0 @@ -load("@rules_cc//cc:cc_library.bzl", "cc_library") - -cc_library( - name = "memcpy_bench", - srcs = ["memcpy_bench.cc"], - hdrs = ["memcpy_bench.h"], - deps = [ - "@google_benchmark//:benchmark", - ], - visibility = [ - "//main:__pkg__", - "//test:__pkg__", - ], -) diff --git a/src/lib/memcpy_bench.cc b/src/lib/memcpy_bench.cc deleted file mode 100644 index 46585bd..0000000 --- a/src/lib/memcpy_bench.cc +++ /dev/null @@ -1,54 +0,0 @@ -#include "lib/memcpy_bench.h" - -#include -#include - -#include - -namespace memcpy_bench { - -namespace { - -void BM_Memcpy(benchmark::State& state) { - const auto size = static_cast(state.range(0)); - - const std::vector src = RandomBytes(size); - std::vector dst(size); - - for (auto _ : state) { - std::memcpy(dst.data(), src.data(), size); - benchmark::DoNotOptimize(dst.data()); - benchmark::ClobberMemory(); - } - - state.SetBytesProcessed(static_cast(state.iterations()) * - static_cast(size)); -} - -} - -std::vector RandomBytes(std::size_t size, std::uint32_t seed) { - std::mt19937 gen(seed); - std::uniform_int_distribution dist(0, 255); - std::vector bytes(size); - for (auto& byte : bytes) { - byte = static_cast(dist(gen)); - } - return bytes; -} - -int Run(int argc, char** argv) { - benchmark::RegisterBenchmark("BM_Memcpy", BM_Memcpy) - ->RangeMultiplier(4) - ->Range(64, 64 << 20); - - benchmark::Initialize(&argc, argv); - if (benchmark::ReportUnrecognizedArguments(argc, argv)) { - return 1; - } - benchmark::RunSpecifiedBenchmarks(); - benchmark::Shutdown(); - return 0; -} - -} diff --git a/src/lib/memcpy_bench.h b/src/lib/memcpy_bench.h deleted file mode 100644 index bd827d5..0000000 --- a/src/lib/memcpy_bench.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -#include -#include -#include - -namespace memcpy_bench { - -std::vector RandomBytes(std::size_t size, std::uint32_t seed = 42); - -int Run(int argc, char** argv); - -} diff --git a/src/main/BUILD b/src/main/BUILD deleted file mode 100644 index 89b4608..0000000 --- a/src/main/BUILD +++ /dev/null @@ -1,9 +0,0 @@ -load("@rules_cc//cc:cc_binary.bzl", "cc_binary") - -cc_binary( - name = "main", - srcs = glob(["*.cc"]), - deps = [ - "//lib:memcpy_bench", - ], -) diff --git a/src/main/main.cc b/src/main/main.cc deleted file mode 100644 index 0bcf91d..0000000 --- a/src/main/main.cc +++ /dev/null @@ -1,6 +0,0 @@ -#include "lib/memcpy_bench.h" - -int main(int argc, char **argv) -{ - return memcpy_bench::Run(argc, argv); -} diff --git a/src/test/BUILD b/src/test/BUILD deleted file mode 100644 index 750aa32..0000000 --- a/src/test/BUILD +++ /dev/null @@ -1,11 +0,0 @@ -load("@rules_cc//cc:cc_test.bzl", "cc_test") - - -cc_test( - name = "memcpy_bench_test", - srcs = glob(["*.cc"]), - deps = [ - "@googletest//:gtest_main", - "//lib:memcpy_bench", - ], -) diff --git a/src/test/memcpy_bench_test.cc b/src/test/memcpy_bench_test.cc deleted file mode 100644 index c7ab159..0000000 --- a/src/test/memcpy_bench_test.cc +++ /dev/null @@ -1,15 +0,0 @@ -#include "gtest/gtest.h" -#include "lib/memcpy_bench.h" - -TEST(RandomBytesTest, ReturnsRequestedSize) { - EXPECT_EQ(memcpy_bench::RandomBytes(0).size(), 0u); - EXPECT_EQ(memcpy_bench::RandomBytes(1024).size(), 1024u); -} - -TEST(RandomBytesTest, SameSeedGivesSameBytes) { - EXPECT_EQ(memcpy_bench::RandomBytes(256, 7), memcpy_bench::RandomBytes(256, 7)); -} - -TEST(RandomBytesTest, DifferentSeedsGiveDifferentBytes) { - EXPECT_NE(memcpy_bench::RandomBytes(256, 1), memcpy_bench::RandomBytes(256, 2)); -} diff --git a/tools/.gitkeep b/tools/.gitkeep new file mode 100644 index 0000000..9b0b175 --- /dev/null +++ b/tools/.gitkeep @@ -0,0 +1 @@ +# Experiment automation and analysis utilities belong in this directory. diff --git a/tools/run-clang-tidy.sh b/tools/run-clang-tidy.sh new file mode 100644 index 0000000..a05d506 --- /dev/null +++ b/tools/run-clang-tidy.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env sh +set -eu + +if ! command -v clang-tidy >/dev/null 2>&1; then + echo "clang-tidy is required but was not found on PATH." >&2 + exit 1 +fi + +bazel run @hedron_compile_commands//:refresh_all + +find lib benchmarks -type f \( -name '*.cc' -o -name '*.cpp' -o -name '*.cxx' \) \ + -exec clang-tidy -p . {} +