diff --git a/benchmarks/bench_format_comparison.cpp b/benchmarks/bench_format_comparison.cpp index c3ed617..94f152c 100644 --- a/benchmarks/bench_format_comparison.cpp +++ b/benchmarks/bench_format_comparison.cpp @@ -15,7 +15,8 @@ // Test data for consistent benchmarks constexpr int TEST_INT = 42; -constexpr double TEST_DOUBLE = 3.14159; +constexpr double TEST_DOUBLE = + 3.141592653589793; // Exact IEEE 754 representation of π const std::string TEST_STRING = "Hello World"; // Benchmark simple string concatenation: "Hello " + "World" + "!" @@ -23,20 +24,21 @@ 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); + bench.title("Simple Concatenation").unit("op").warmup(100); + ArenaAllocator arena(64); // Arena-based static_format bench.run("static_format", [&] { - ArenaAllocator arena(64); auto result = static_format(arena, "Hello ", "World", "!"); ankerl::nanobench::doNotOptimizeAway(result); + arena.reset(); }); // Arena-based format bench.run("format", [&] { - ArenaAllocator arena(64); auto result = format(arena, "Hello %s!", "World"); ankerl::nanobench::doNotOptimizeAway(result); + arena.reset(); }); // std::stringstream @@ -54,16 +56,6 @@ void benchmark_simple_concatenation() { 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" @@ -71,21 +63,22 @@ 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); + bench.title("Mixed Types").unit("op").warmup(100); + ArenaAllocator arena(128); // 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.reset(); }); // 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); + arena.reset(); }); // std::stringstream @@ -104,17 +97,6 @@ void benchmark_mixed_types() { 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 @@ -122,14 +104,15 @@ 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); + bench.title("Complex Formatting").unit("op").warmup(100); + ArenaAllocator arena(128); // 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); + arena.reset(); }); // std::stringstream @@ -150,17 +133,6 @@ void benchmark_complex_formatting() { 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) @@ -169,26 +141,27 @@ void benchmark_error_messages() { "(line 123)' ===\n"; ankerl::nanobench::Bench bench; - bench.title("Error Messages").unit("op").warmup(100).epochs(1000); + bench.title("Error Messages").unit("op").warmup(100); constexpr int error_code = 404; constexpr int line_number = 123; const std::string error_msg = "File not found"; + ArenaAllocator arena(128); // Arena-based static_format (using string literals only) bench.run("static_format", [&] { - ArenaAllocator arena(128); auto result = static_format(arena, "Error ", error_code, ": ", "File not found", " (line ", line_number, ")"); ankerl::nanobench::doNotOptimizeAway(result); + arena.reset(); }); // 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); + arena.reset(); }); // std::stringstream @@ -210,37 +183,73 @@ void benchmark_error_messages() { #endif } -// Benchmark memory allocation overhead by testing arena reuse -void benchmark_memory_reuse() { - std::cout << "\n=== Memory Allocation Patterns ===\n"; +// Benchmark simple double formatting (common in metrics) +void benchmark_double_formatting() { + std::cout << "\n=== Simple Double Formatting ===\n"; + + // Validate that all formatters produce identical output + ArenaAllocator arena(128); + + auto static_result = static_format(arena, TEST_DOUBLE); + auto format_result = format(arena, "%.17g", TEST_DOUBLE); + + std::stringstream ss; + ss << std::setprecision(17) << TEST_DOUBLE; + auto stringstream_result = ss.str(); + +#if HAS_STD_FORMAT + auto std_format_result = std::format("{}", TEST_DOUBLE); +#endif + + std::cout << "Validation (note: precision algorithms may differ):\n"; + std::cout << " static_format: '" << static_result + << "' (length: " << static_result.length() << ")\n"; + std::cout << " format(%.17g): '" << format_result + << "' (length: " << format_result.length() << ")\n"; + std::cout << " std::stringstream: '" << stringstream_result + << "' (length: " << stringstream_result.length() << ")\n"; +#if HAS_STD_FORMAT + std::cout << " std::format: '" << std_format_result + << "' (length: " << std_format_result.length() << ")\n"; +#endif + + std::cout + << "Note: Different formatters may use different precision algorithms\n"; + std::cout << "Proceeding with performance comparison...\n"; ankerl::nanobench::Bench bench; - bench.title("Memory Patterns").unit("op").warmup(100).epochs(100); + bench.title("Double Formatting").unit("op").warmup(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); + // Arena-based static_format (double only) + bench.run("static_format(double)", [&] { + auto result = static_format(arena, TEST_DOUBLE); ankerl::nanobench::doNotOptimizeAway(result); + arena.reset(); }); - // 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); + // Arena-based format with equivalent precision + bench.run("format(%.17g)", [&] { + // Use %.17g to match static_format's full precision behavior + auto result = format(arena, "%.17g", TEST_DOUBLE); ankerl::nanobench::doNotOptimizeAway(result); + arena.reset(); }); - // Fresh std::string allocations + // std::stringstream (full precision) bench.run("std::stringstream", [&] { std::stringstream ss; - ss << "Test " << TEST_INT << ": " << TEST_STRING << " " << std::fixed - << std::setprecision(2) << TEST_DOUBLE; + ss << std::setprecision(17) << TEST_DOUBLE; auto result = ss.str(); ankerl::nanobench::doNotOptimizeAway(result); }); + +#if HAS_STD_FORMAT + // std::format (C++20) - default formatting + bench.run("std::format", [&] { + auto result = std::format("{}", TEST_DOUBLE); + ankerl::nanobench::doNotOptimizeAway(result); + }); +#endif } int main() { @@ -257,7 +266,7 @@ int main() { benchmark_mixed_types(); benchmark_complex_formatting(); benchmark_error_messages(); - benchmark_memory_reuse(); + benchmark_double_formatting(); std::cout << "\n=== Summary ===\n"; std::cout @@ -267,8 +276,6 @@ int main() { 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/benchmarks/bench_metric.cpp b/benchmarks/bench_metric.cpp index e829730..7e744bd 100644 --- a/benchmarks/bench_metric.cpp +++ b/benchmarks/bench_metric.cpp @@ -156,50 +156,6 @@ int main() { [&]() { histogram.observe(2.0); }); } - // Render performance scaling - { - // Test render performance as number of metrics increases - std::vector counters; - std::vector gauges; - std::vector histograms; - - auto counter_family = - metric::create_counter("scale_counter", "Scale counter"); - auto gauge_family = metric::create_gauge("scale_gauge", "Scale gauge"); - auto histogram_family = metric::create_histogram( - "scale_histogram", "Scale histogram", - std::initializer_list{0.1, 0.5, 1.0, 2.5, 5.0, 10.0, 25.0, - 50.0}); - - // Create varying numbers of metrics - for (int scale : {10, 100, 1000}) { - // Clear previous metrics by creating new families - // (Note: In real usage, metrics persist for application lifetime) - for (int i = 0; i < scale; ++i) { - counters.emplace_back( - counter_family.create({{"id", std::to_string(i)}})); - gauges.emplace_back(gauge_family.create({{"id", std::to_string(i)}})); - histograms.emplace_back( - histogram_family.create({{"id", std::to_string(i)}})); - - // Set some values - counters.back().inc(static_cast(i)); - gauges.back().set(static_cast(i * 2)); - histograms.back().observe(static_cast(i) * 0.1); - } - - ArenaAllocator arena; - std::string bench_name = - "render() - " + std::to_string(scale) + " metrics each type"; - - bench.run(bench_name, [&]() { - auto output = metric::render(arena); - ankerl::nanobench::doNotOptimizeAway(output); - arena.reset(); - }); - } - } - // Callback metrics performance { auto counter_family = @@ -245,5 +201,51 @@ int main() { callback_updater.join(); } + // Render performance scaling + { + bench.unit("metric"); + // Test render performance as number of metrics increases + std::vector counters; + std::vector gauges; + std::vector histograms; + + auto counter_family = + metric::create_counter("scale_counter", "Scale counter"); + auto gauge_family = metric::create_gauge("scale_gauge", "Scale gauge"); + auto histogram_family = metric::create_histogram( + "scale_histogram", "Scale histogram", + std::initializer_list{0.1, 0.5, 1.0, 2.5, 5.0, 10.0, 25.0, + 50.0}); + + // Create varying numbers of metrics + for (int scale : {10, 100, 1000}) { + bench.batch(scale); + // Clear previous metrics by creating new families + // (Note: In real usage, metrics persist for application lifetime) + for (int i = 0; i < scale; ++i) { + counters.emplace_back( + counter_family.create({{"id", std::to_string(i)}})); + gauges.emplace_back(gauge_family.create({{"id", std::to_string(i)}})); + histograms.emplace_back( + histogram_family.create({{"id", std::to_string(i)}})); + + // Set some values + counters.back().inc(static_cast(i)); + gauges.back().set(static_cast(i * 2)); + histograms.back().observe(static_cast(i) * 0.1); + } + + ArenaAllocator arena; + std::string bench_name = + "render() - " + std::to_string(scale) + " metrics each type"; + + bench.run(bench_name, [&]() { + auto output = metric::render(arena); + ankerl::nanobench::doNotOptimizeAway(output); + arena.reset(); + }); + } + } + return 0; } diff --git a/src/format.cpp b/src/format.cpp index 9b9c9dd..d3ea57c 100644 --- a/src/format.cpp +++ b/src/format.cpp @@ -948,13 +948,7 @@ char *to_chars(char *first, const char *last, double value) { } // namespace namespace detail { -void DoubleTerm::write(char *&buf) const { - char scratch[kMaxLength]; - char *end = to_chars(scratch, nullptr, s); - const auto len = end - scratch; - std::memcpy(buf, scratch, static_cast(len)); - buf += len; -} +void DoubleTerm::write(char *&buf) const { buf = to_chars(buf, nullptr, s); } } // namespace detail