diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 77d6d1a..f9f6c61 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -35,6 +35,14 @@ jobs: working-directory: ctprintf run: cmake --workflow --preset release-workflow + - name: Run performance benchmarks + working-directory: ctprintf + run: | + cmake --workflow --fresh --preset benchmark-workflow + ./build/benchmark/benchmarks/ctprintf_benchmarks \ + --benchmark_min_time=1s \ + --benchmark_repetitions=3 + - name: Build Debian package run: ./scripts/build-package.sh diff --git a/README.md b/README.md index 6385e53..30142bf 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,20 @@ cmake --build build ctest --test-dir build --output-on-failure ``` +## Benchmarking + +The optional benchmarks compare `ctprintf` with `std::snprintf` using the same +printf-style formats and fixed-size output buffers. They require Google +Benchmark: + +```bash +cmake --workflow --preset benchmark-workflow +./ctprintf/build/benchmark/benchmarks/ctprintf_benchmarks +``` + +The benchmark target is separate from CTest because execution time varies +between machines and system load. + ## Creating a package CPack creates a gzip-compressed tarball containing the installable headers, diff --git a/ctprintf/CMakeLists.txt b/ctprintf/CMakeLists.txt index c11cb43..218d34b 100644 --- a/ctprintf/CMakeLists.txt +++ b/ctprintf/CMakeLists.txt @@ -13,6 +13,7 @@ include(GNUInstallDirs) include(CMakePackageConfigHelpers) option(ENABLE_TESTING "Build and enable tests" OFF) +option(ENABLE_BENCHMARKS "Build performance benchmarks" OFF) add_library(ctprintf INTERFACE) @@ -62,6 +63,10 @@ if(ENABLE_TESTING) add_subdirectory(tests) endif() # ENABLE_TESTING +if(ENABLE_BENCHMARKS) + add_subdirectory(benchmarks) +endif() # ENABLE_BENCHMARKS + set(CPACK_GENERATOR "TGZ") set(CPACK_PACKAGE_CONTACT "Thomas Willetal ") set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Compile-time checked printf-style formatting library") diff --git a/ctprintf/CMakePresets.json b/ctprintf/CMakePresets.json index 87686aa..a508a5f 100644 --- a/ctprintf/CMakePresets.json +++ b/ctprintf/CMakePresets.json @@ -35,6 +35,15 @@ "cacheVariables": { "CMAKE_CXX_CLANG_TIDY": "clang-tidy;--warnings-as-errors=*" } + }, + { + "name": "benchmark", + "displayName": "Benchmark", + "inherits": "release", + "cacheVariables": { + "ENABLE_BENCHMARKS": "ON", + "ENABLE_TESTING": "OFF" + } } ], "buildPresets": [ @@ -49,6 +58,10 @@ { "name": "clang-tidy", "configurePreset": "clang-tidy" + }, + { + "name": "benchmark", + "configurePreset": "benchmark" } ], "testPresets": [ @@ -135,6 +148,20 @@ "name": "clang-tidy" } ] + }, + { + "name": "benchmark-workflow", + "displayName": "Benchmark: configure and build", + "steps": [ + { + "type": "configure", + "name": "benchmark" + }, + { + "type": "build", + "name": "benchmark" + } + ] } ] } diff --git a/ctprintf/benchmarks/CMakeLists.txt b/ctprintf/benchmarks/CMakeLists.txt new file mode 100644 index 0000000..d82ba04 --- /dev/null +++ b/ctprintf/benchmarks/CMakeLists.txt @@ -0,0 +1,5 @@ +find_package(benchmark CONFIG REQUIRED) + +add_executable(ctprintf_benchmarks formatting.cpp) + +target_link_libraries(ctprintf_benchmarks PRIVATE ctprintf::ctprintf benchmark::benchmark) diff --git a/ctprintf/benchmarks/formatting.cpp b/ctprintf/benchmarks/formatting.cpp new file mode 100644 index 0000000..7f9c3a3 --- /dev/null +++ b/ctprintf/benchmarks/formatting.cpp @@ -0,0 +1,86 @@ +#include + +#include + +#include +#include +#include + +namespace { + +class Buffer { + public: + void clear() { size_ = 0; } + + void put(char character) { characters_[size_++] = character; } + + private: + std::array characters_{}; + std::size_t size_ = 0; +}; + +void benchmark_ctprintf_hexadecimal(benchmark::State &state) +{ + constexpr unsigned int value = 0xDEADBEEFU; + Buffer output; + + for (auto _ : state) { + output.clear(); + ctprintf::format(output, "value=%08x", value); + benchmark::DoNotOptimize(output); + benchmark::ClobberMemory(); + } +} + +void benchmark_snprintf_hexadecimal(benchmark::State &state) +{ + constexpr unsigned int value = 0xDEADBEEFU; + std::array output{}; + + for (auto _ : state) { + int result = std::snprintf(output.data(), output.size(), "value=%08x", value); + benchmark::DoNotOptimize(result); + benchmark::DoNotOptimize(output); + benchmark::ClobberMemory(); + } +} + +void benchmark_ctprintf_mixed(benchmark::State &state) +{ + constexpr unsigned int identifier = 0x42U; + constexpr int status = -17; + constexpr const char *name = "sensor"; + Buffer output; + + for (auto _ : state) { + output.clear(); + ctprintf::format(output, "id=%08x status=%+d name=%s", identifier, status, name); + benchmark::DoNotOptimize(output); + benchmark::ClobberMemory(); + } +} + +void benchmark_snprintf_mixed(benchmark::State &state) +{ + constexpr unsigned int identifier = 0x42U; + constexpr int status = -17; + constexpr const char *name = "sensor"; + std::array output{}; + + for (auto _ : state) { + int result = std::snprintf(output.data(), output.size(), "id=%08x status=%+d name=%s", + identifier, status, name); + benchmark::DoNotOptimize(result); + benchmark::DoNotOptimize(output); + benchmark::ClobberMemory(); + } +} + +} // namespace + +BENCHMARK(benchmark_ctprintf_hexadecimal); +BENCHMARK(benchmark_snprintf_hexadecimal); +BENCHMARK(benchmark_ctprintf_mixed); +BENCHMARK(benchmark_snprintf_mixed); + +BENCHMARK_MAIN(); diff --git a/scripts/build-dependencies.sh b/scripts/build-dependencies.sh index 63d04e5..7b8f2de 100755 --- a/scripts/build-dependencies.sh +++ b/scripts/build-dependencies.sh @@ -10,6 +10,7 @@ sudo DEBIAN_FRONTEND=noninteractive apt-get install --yes --no-install-recommend cmake \ debhelper \ devscripts \ + libbenchmark-dev \ libgtest-dev \ ninja-build \ pipx