diff --git a/CMakeLists.txt b/CMakeLists.txt index 22b3863..234562f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -217,6 +217,11 @@ add_executable(bench_thread_pipeline benchmarks/bench_thread_pipeline.cpp) target_link_libraries(bench_thread_pipeline nanobench Threads::Threads) target_include_directories(bench_thread_pipeline PRIVATE src) +add_executable(bench_format_comparison benchmarks/bench_format_comparison.cpp + src/arena_allocator.cpp src/format.cpp) +target_link_libraries(bench_format_comparison nanobench) +target_include_directories(bench_format_comparison PRIVATE src) + # Debug tools add_executable( debug_arena tools/debug_arena.cpp src/json_commit_request_parser.cpp @@ -238,3 +243,4 @@ add_test(NAME arena_allocator_benchmarks COMMAND bench_arena_allocator) add_test(NAME commit_request_benchmarks COMMAND bench_commit_request) add_test(NAME parser_comparison_benchmarks COMMAND bench_parser_comparison) add_test(NAME thread_pipeline_benchmarks COMMAND bench_thread_pipeline) +add_test(NAME format_comparison_benchmarks COMMAND bench_format_comparison) diff --git a/benchmarks/bench_format_comparison.cpp b/benchmarks/bench_format_comparison.cpp new file mode 100644 index 0000000..b5c41e3 --- /dev/null +++ b/benchmarks/bench_format_comparison.cpp @@ -0,0 +1,275 @@ +#include "arena_allocator.hpp" +#include "format.hpp" +#include +#include +#include +#include +#include + +#if __cpp_lib_format >= 201907L +#include +#define HAS_STD_FORMAT 1 +#else +#define HAS_STD_FORMAT 0 +#endif + +// Test data for consistent benchmarks +constexpr int TEST_INT = 42; +constexpr double TEST_DOUBLE = 3.14159; +const std::string TEST_STRING = "Hello World"; + +// Benchmark simple string concatenation: "Hello " + "World" + "!" +void benchmark_simple_concatenation() { + std::cout << "\n=== Simple String Concatenation: 'Hello World!' ===\n"; + + ankerl::nanobench::Bench bench; + bench.title("Simple Concatenation").unit("op").warmup(100).epochs(1000); + + // Arena-based static_format + bench.run("static_format", [&] { + ArenaAllocator arena(64); + auto result = static_format(arena, "Hello ", "World", "!"); + ankerl::nanobench::doNotOptimizeAway(result); + }); + + // Arena-based format + bench.run("format", [&] { + ArenaAllocator arena(64); + auto result = format(arena, "Hello %s!", "World"); + ankerl::nanobench::doNotOptimizeAway(result); + }); + + // std::stringstream + bench.run("std::stringstream", [&] { + std::stringstream ss; + ss << "Hello " << "World" << "!"; + auto result = ss.str(); + ankerl::nanobench::doNotOptimizeAway(result); + }); + +#if HAS_STD_FORMAT + // std::format (C++20) + bench.run("std::format", [&] { + auto result = std::format("Hello {}!", "World"); + ankerl::nanobench::doNotOptimizeAway(result); + }); +#endif + + // Raw snprintf with malloc + bench.run("snprintf + malloc", [&] { + char buffer[64]; + int len = std::snprintf(buffer, sizeof(buffer), "Hello %s!", "World"); + char *result = static_cast(std::malloc(len + 1)); + std::memcpy(result, buffer, len + 1); + ankerl::nanobench::doNotOptimizeAway(result); + std::free(result); + }); +} + +// Benchmark mixed type formatting: "Count: 42, Rate: 3.14159" +void benchmark_mixed_types() { + std::cout << "\n=== Mixed Type Formatting: 'Count: 42, Rate: 3.14159' ===\n"; + + ankerl::nanobench::Bench bench; + bench.title("Mixed Types").unit("op").warmup(100).epochs(1000); + + // Arena-based static_format + bench.run("static_format", [&] { + ArenaAllocator arena(128); + auto result = + static_format(arena, "Count: ", TEST_INT, ", Rate: ", TEST_DOUBLE); + ankerl::nanobench::doNotOptimizeAway(result); + }); + + // Arena-based format + bench.run("format", [&] { + ArenaAllocator arena(128); + auto result = format(arena, "Count: %d, Rate: %.5f", TEST_INT, TEST_DOUBLE); + ankerl::nanobench::doNotOptimizeAway(result); + }); + + // std::stringstream + bench.run("std::stringstream", [&] { + std::stringstream ss; + ss << "Count: " << TEST_INT << ", Rate: " << std::fixed + << std::setprecision(5) << TEST_DOUBLE; + auto result = ss.str(); + ankerl::nanobench::doNotOptimizeAway(result); + }); + +#if HAS_STD_FORMAT + // std::format (C++20) + bench.run("std::format", [&] { + auto result = std::format("Count: {}, Rate: {:.5f}", TEST_INT, TEST_DOUBLE); + ankerl::nanobench::doNotOptimizeAway(result); + }); +#endif + + // Raw snprintf with malloc + bench.run("snprintf + malloc", [&] { + char buffer[128]; + int len = std::snprintf(buffer, sizeof(buffer), "Count: %d, Rate: %.5f", + TEST_INT, TEST_DOUBLE); + char *result = static_cast(std::malloc(len + 1)); + std::memcpy(result, buffer, len + 1); + ankerl::nanobench::doNotOptimizeAway(result); + std::free(result); + }); +} + +// Benchmark complex formatting with precision and alignment +void benchmark_complex_formatting() { + std::cout << "\n=== Complex Formatting: '%-10s %5d %8.2f' ===\n"; + + ankerl::nanobench::Bench bench; + bench.title("Complex Formatting").unit("op").warmup(100).epochs(1000); + + // Arena-based format (static_format doesn't support printf specifiers) + bench.run("format", [&] { + ArenaAllocator arena(128); + auto result = format(arena, "%-10s %5d %8.2f", TEST_STRING.c_str(), + TEST_INT, TEST_DOUBLE); + ankerl::nanobench::doNotOptimizeAway(result); + }); + + // std::stringstream + bench.run("std::stringstream", [&] { + std::stringstream ss; + ss << std::left << std::setw(10) << TEST_STRING << " " << std::right + << std::setw(5) << TEST_INT << " " << std::setw(8) << std::fixed + << std::setprecision(2) << TEST_DOUBLE; + auto result = ss.str(); + ankerl::nanobench::doNotOptimizeAway(result); + }); + +#if HAS_STD_FORMAT + // std::format (C++20) + bench.run("std::format", [&] { + auto result = std::format("{:<10} {:>5} {:>8.2f}", TEST_STRING, TEST_INT, + TEST_DOUBLE); + ankerl::nanobench::doNotOptimizeAway(result); + }); +#endif + + // Raw snprintf with malloc + bench.run("snprintf + malloc", [&] { + char buffer[128]; + int len = std::snprintf(buffer, sizeof(buffer), "%-10s %5d %8.2f", + TEST_STRING.c_str(), TEST_INT, TEST_DOUBLE); + char *result = static_cast(std::malloc(len + 1)); + std::memcpy(result, buffer, len + 1); + ankerl::nanobench::doNotOptimizeAway(result); + std::free(result); + }); +} + +// Benchmark error message formatting (common use case) +void benchmark_error_messages() { + std::cout << "\n=== Error Message Formatting: 'Error 404: File not found " + "(line 123)' ===\n"; + + ankerl::nanobench::Bench bench; + bench.title("Error Messages").unit("op").warmup(100).epochs(1000); + + constexpr int error_code = 404; + constexpr int line_number = 123; + const std::string error_msg = "File not found"; + + // Arena-based static_format (using std::string_view) + bench.run("static_format", [&] { + ArenaAllocator arena(128); + std::string_view error_msg_sv = error_msg; + auto result = static_format(arena, "Error ", error_code, ": ", error_msg_sv, + " (line ", line_number, ")"); + ankerl::nanobench::doNotOptimizeAway(result); + }); + + // Arena-based format + bench.run("format", [&] { + ArenaAllocator arena(128); + auto result = format(arena, "Error %d: %s (line %d)", error_code, + error_msg.c_str(), line_number); + ankerl::nanobench::doNotOptimizeAway(result); + }); + + // std::stringstream + bench.run("std::stringstream", [&] { + std::stringstream ss; + ss << "Error " << error_code << ": " << error_msg << " (line " + << line_number << ")"; + auto result = ss.str(); + ankerl::nanobench::doNotOptimizeAway(result); + }); + +#if HAS_STD_FORMAT + // std::format (C++20) + bench.run("std::format", [&] { + auto result = std::format("Error {}: {} (line {})", error_code, error_msg, + line_number); + ankerl::nanobench::doNotOptimizeAway(result); + }); +#endif +} + +// Benchmark memory allocation overhead by testing arena reuse +void benchmark_memory_reuse() { + std::cout << "\n=== Memory Allocation Patterns ===\n"; + + ankerl::nanobench::Bench bench; + bench.title("Memory Patterns").unit("op").warmup(100).epochs(100); + + // Arena with fresh allocation each time (realistic usage) + bench.run("fresh arena", [&] { + ArenaAllocator arena(128); + auto result = format(arena, "Test %d: %s %.2f", TEST_INT, + TEST_STRING.c_str(), TEST_DOUBLE); + ankerl::nanobench::doNotOptimizeAway(result); + }); + + // Pre-allocated arena (reuse scenario) + ArenaAllocator shared_arena(1024); + bench.run("reused arena", [&] { + auto result = format(shared_arena, "Test %d: %s %.2f", TEST_INT, + TEST_STRING.c_str(), TEST_DOUBLE); + ankerl::nanobench::doNotOptimizeAway(result); + }); + + // Fresh std::string allocations + bench.run("std::stringstream", [&] { + std::stringstream ss; + ss << "Test " << TEST_INT << ": " << TEST_STRING << " " << std::fixed + << std::setprecision(2) << TEST_DOUBLE; + auto result = ss.str(); + ankerl::nanobench::doNotOptimizeAway(result); + }); +} + +int main() { + std::cout << "Format Function Benchmark Comparison\n"; + std::cout << "====================================\n"; + +#if HAS_STD_FORMAT + std::cout << "C++20 std::format: Available\n"; +#else + std::cout << "C++20 std::format: Not available\n"; +#endif + + benchmark_simple_concatenation(); + benchmark_mixed_types(); + benchmark_complex_formatting(); + benchmark_error_messages(); + benchmark_memory_reuse(); + + std::cout << "\n=== Summary ===\n"; + std::cout + << "* static_format: Best for simple concatenation with known types\n"; + std::cout + << "* format: Best for printf-style formatting with arena allocation\n"; + std::cout + << "* std::stringstream: Flexible but slower due to heap allocation\n"; + std::cout << "* std::format: Modern C++20 alternative (if available)\n"; + std::cout << "* snprintf + malloc: Low-level but requires manual memory " + "management\n"; + + return 0; +} diff --git a/src/format.hpp b/src/format.hpp index 8d454d3..f3eb73e 100644 --- a/src/format.hpp +++ b/src/format.hpp @@ -254,6 +254,25 @@ inline constexpr int max_decimal_length_v = decltype(term(T{}))::kMaxLength; inline constexpr DoubleTerm term(double s) { return DoubleTerm(s); } +// Runtime string term for std::string_view (works with std::string, const +// char*, etc.) +struct StringViewTerm { + explicit constexpr StringViewTerm(std::string_view s) : s(s) {} + static constexpr int kMaxLength = + 512; // Conservative upper bound for runtime strings + void write(char *&buf) const { + std::memcpy(buf, s.data(), s.size()); + buf += s.size(); + } + +private: + std::string_view s; +}; + +inline constexpr StringViewTerm term(std::string_view s) { + return StringViewTerm(s); +} + } // namespace detail /**