From 2a7c1908165b96a86b333d52261ab5d84297e6f4 Mon Sep 17 00:00:00 2001 From: gen740 <54583542+gen740@users.noreply.github.com> Date: Tue, 12 May 2026 19:58:35 +0900 Subject: [PATCH 01/15] Add parser benchmark suite --- .gitignore | 1 + bench/CMakeLists.txt | 12 ++ bench/cmake/BenchLibraries.cmake | 39 +++++ .../cases/many_options/argparse.cc | 1 + .../compile_time/cases/many_options/cli11.cc | 1 + .../compile_time/cases/many_options/cli20.cc | 1 + .../cases/many_options/cxxopts.cc | 1 + bench/compile_time/cases/simple/argparse.cc | 1 + bench/compile_time/cases/simple/cli11.cc | 1 + bench/compile_time/cases/simple/cli20.cc | 1 + bench/compile_time/cases/simple/cxxopts.cc | 1 + .../compile_time/cases/subcommand/argparse.cc | 1 + bench/compile_time/cases/subcommand/cli11.cc | 1 + bench/compile_time/cases/subcommand/cli20.cc | 1 + .../compile_time/cases/subcommand/cxxopts.cc | 1 + bench/compile_time/scripts/compile-one.sh | 31 ++++ bench/compile_time/scripts/run.sh | 25 +++ bench/include/bench/allocation_counter.hh | 44 ++++++ bench/include/bench/argv.hh | 43 ++++++ bench/results/.gitignore | 3 + bench/results/.gitkeep | 1 + bench/runtime/CMakeLists.txt | 15 ++ bench/runtime/many_options.cc | 144 ++++++++++++++++++ bench/runtime/simple.cc | 81 ++++++++++ bench/runtime/subcommand.cc | 89 +++++++++++ bench/scripts/run-runtime.sh | 15 ++ bench/scripts/run-size.sh | 23 +++ bench/scripts/summarize.py | 52 +++++++ bench/size/CMakeLists.txt | 19 +++ bench/size/many_options/argparse.cc | 27 ++++ bench/size/many_options/cli11.cc | 29 ++++ bench/size/many_options/cli20.cc | 26 ++++ bench/size/many_options/cxxopts.cc | 28 ++++ bench/size/simple/argparse.cc | 14 ++ bench/size/simple/cli11.cc | 18 +++ bench/size/simple/cli20.cc | 15 ++ bench/size/simple/cxxopts.cc | 17 +++ bench/size/subcommand/argparse.cc | 17 +++ bench/size/subcommand/cli11.cc | 18 +++ bench/size/subcommand/cli20.cc | 20 +++ bench/size/subcommand/cxxopts.cc | 25 +++ flake.nix | 35 +++++ 42 files changed, 938 insertions(+) create mode 100644 bench/CMakeLists.txt create mode 100644 bench/cmake/BenchLibraries.cmake create mode 100644 bench/compile_time/cases/many_options/argparse.cc create mode 100644 bench/compile_time/cases/many_options/cli11.cc create mode 100644 bench/compile_time/cases/many_options/cli20.cc create mode 100644 bench/compile_time/cases/many_options/cxxopts.cc create mode 100644 bench/compile_time/cases/simple/argparse.cc create mode 100644 bench/compile_time/cases/simple/cli11.cc create mode 100644 bench/compile_time/cases/simple/cli20.cc create mode 100644 bench/compile_time/cases/simple/cxxopts.cc create mode 100644 bench/compile_time/cases/subcommand/argparse.cc create mode 100644 bench/compile_time/cases/subcommand/cli11.cc create mode 100644 bench/compile_time/cases/subcommand/cli20.cc create mode 100644 bench/compile_time/cases/subcommand/cxxopts.cc create mode 100755 bench/compile_time/scripts/compile-one.sh create mode 100755 bench/compile_time/scripts/run.sh create mode 100644 bench/include/bench/allocation_counter.hh create mode 100644 bench/include/bench/argv.hh create mode 100644 bench/results/.gitignore create mode 100644 bench/results/.gitkeep create mode 100644 bench/runtime/CMakeLists.txt create mode 100644 bench/runtime/many_options.cc create mode 100644 bench/runtime/simple.cc create mode 100644 bench/runtime/subcommand.cc create mode 100755 bench/scripts/run-runtime.sh create mode 100755 bench/scripts/run-size.sh create mode 100755 bench/scripts/summarize.py create mode 100644 bench/size/CMakeLists.txt create mode 100644 bench/size/many_options/argparse.cc create mode 100644 bench/size/many_options/cli11.cc create mode 100644 bench/size/many_options/cli20.cc create mode 100644 bench/size/many_options/cxxopts.cc create mode 100644 bench/size/simple/argparse.cc create mode 100644 bench/size/simple/cli11.cc create mode 100644 bench/size/simple/cli20.cc create mode 100644 bench/size/simple/cxxopts.cc create mode 100644 bench/size/subcommand/argparse.cc create mode 100644 bench/size/subcommand/cli11.cc create mode 100644 bench/size/subcommand/cli20.cc create mode 100644 bench/size/subcommand/cxxopts.cc diff --git a/.gitignore b/.gitignore index 3f5e0d3..c7797da 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ # build artifacts build/ +build-bench/ *.o *.a *.so diff --git a/bench/CMakeLists.txt b/bench/CMakeLists.txt new file mode 100644 index 0000000..4a51353 --- /dev/null +++ b/bench/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.20) + +project(cli20_bench LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +include(cmake/BenchLibraries.cmake) + +add_subdirectory(runtime) +add_subdirectory(size) diff --git a/bench/cmake/BenchLibraries.cmake b/bench/cmake/BenchLibraries.cmake new file mode 100644 index 0000000..e1efc30 --- /dev/null +++ b/bench/cmake/BenchLibraries.cmake @@ -0,0 +1,39 @@ +include_guard(GLOBAL) + +get_filename_component(CLI20_REPOSITORY_ROOT "${CMAKE_CURRENT_LIST_DIR}/../.." ABSOLUTE) + +add_library(bench_common INTERFACE) +target_include_directories(bench_common INTERFACE + "${CLI20_REPOSITORY_ROOT}/include" + "${CLI20_REPOSITORY_ROOT}/bench/include" +) +target_compile_features(bench_common INTERFACE cxx_std_20) + +add_library(bench_warnings INTERFACE) +target_compile_options(bench_warnings INTERFACE + $,/W4,-Wall;-Wextra;-Wpedantic> +) + +find_package(benchmark REQUIRED) + +find_path(BENCH_CLI11_INCLUDE_DIR CLI/CLI.hpp REQUIRED) +find_path(BENCH_ARGPARSE_INCLUDE_DIR argparse/argparse.hpp REQUIRED) +find_path(BENCH_CXXOPTS_INCLUDE_DIR cxxopts.hpp REQUIRED) + +add_library(bench_cli11 INTERFACE) +target_include_directories(bench_cli11 INTERFACE "${BENCH_CLI11_INCLUDE_DIR}") + +add_library(bench_argparse INTERFACE) +target_include_directories(bench_argparse INTERFACE "${BENCH_ARGPARSE_INCLUDE_DIR}") + +add_library(bench_cxxopts INTERFACE) +target_include_directories(bench_cxxopts INTERFACE "${BENCH_CXXOPTS_INCLUDE_DIR}") + +function(bench_add_size_executable target source library_target) + add_executable("${target}" "${source}") + target_link_libraries("${target}" PRIVATE + bench_common + bench_warnings + "${library_target}" + ) +endfunction() diff --git a/bench/compile_time/cases/many_options/argparse.cc b/bench/compile_time/cases/many_options/argparse.cc new file mode 100644 index 0000000..b0db921 --- /dev/null +++ b/bench/compile_time/cases/many_options/argparse.cc @@ -0,0 +1 @@ +#include "../../../size/many_options/argparse.cc" diff --git a/bench/compile_time/cases/many_options/cli11.cc b/bench/compile_time/cases/many_options/cli11.cc new file mode 100644 index 0000000..e1e6303 --- /dev/null +++ b/bench/compile_time/cases/many_options/cli11.cc @@ -0,0 +1 @@ +#include "../../../size/many_options/cli11.cc" diff --git a/bench/compile_time/cases/many_options/cli20.cc b/bench/compile_time/cases/many_options/cli20.cc new file mode 100644 index 0000000..497f96f --- /dev/null +++ b/bench/compile_time/cases/many_options/cli20.cc @@ -0,0 +1 @@ +#include "../../../size/many_options/cli20.cc" diff --git a/bench/compile_time/cases/many_options/cxxopts.cc b/bench/compile_time/cases/many_options/cxxopts.cc new file mode 100644 index 0000000..675a63b --- /dev/null +++ b/bench/compile_time/cases/many_options/cxxopts.cc @@ -0,0 +1 @@ +#include "../../../size/many_options/cxxopts.cc" diff --git a/bench/compile_time/cases/simple/argparse.cc b/bench/compile_time/cases/simple/argparse.cc new file mode 100644 index 0000000..9da3aea --- /dev/null +++ b/bench/compile_time/cases/simple/argparse.cc @@ -0,0 +1 @@ +#include "../../../size/simple/argparse.cc" diff --git a/bench/compile_time/cases/simple/cli11.cc b/bench/compile_time/cases/simple/cli11.cc new file mode 100644 index 0000000..8123b53 --- /dev/null +++ b/bench/compile_time/cases/simple/cli11.cc @@ -0,0 +1 @@ +#include "../../../size/simple/cli11.cc" diff --git a/bench/compile_time/cases/simple/cli20.cc b/bench/compile_time/cases/simple/cli20.cc new file mode 100644 index 0000000..246d02d --- /dev/null +++ b/bench/compile_time/cases/simple/cli20.cc @@ -0,0 +1 @@ +#include "../../../size/simple/cli20.cc" diff --git a/bench/compile_time/cases/simple/cxxopts.cc b/bench/compile_time/cases/simple/cxxopts.cc new file mode 100644 index 0000000..1f5df00 --- /dev/null +++ b/bench/compile_time/cases/simple/cxxopts.cc @@ -0,0 +1 @@ +#include "../../../size/simple/cxxopts.cc" diff --git a/bench/compile_time/cases/subcommand/argparse.cc b/bench/compile_time/cases/subcommand/argparse.cc new file mode 100644 index 0000000..82b9ec0 --- /dev/null +++ b/bench/compile_time/cases/subcommand/argparse.cc @@ -0,0 +1 @@ +#include "../../../size/subcommand/argparse.cc" diff --git a/bench/compile_time/cases/subcommand/cli11.cc b/bench/compile_time/cases/subcommand/cli11.cc new file mode 100644 index 0000000..12f7e40 --- /dev/null +++ b/bench/compile_time/cases/subcommand/cli11.cc @@ -0,0 +1 @@ +#include "../../../size/subcommand/cli11.cc" diff --git a/bench/compile_time/cases/subcommand/cli20.cc b/bench/compile_time/cases/subcommand/cli20.cc new file mode 100644 index 0000000..acc62e4 --- /dev/null +++ b/bench/compile_time/cases/subcommand/cli20.cc @@ -0,0 +1 @@ +#include "../../../size/subcommand/cli20.cc" diff --git a/bench/compile_time/cases/subcommand/cxxopts.cc b/bench/compile_time/cases/subcommand/cxxopts.cc new file mode 100644 index 0000000..1d4c1c6 --- /dev/null +++ b/bench/compile_time/cases/subcommand/cxxopts.cc @@ -0,0 +1 @@ +#include "../../../size/subcommand/cxxopts.cc" diff --git a/bench/compile_time/scripts/compile-one.sh b/bench/compile_time/scripts/compile-one.sh new file mode 100755 index 0000000..4ae2d6c --- /dev/null +++ b/bench/compile_time/scripts/compile-one.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [ "$#" -ne 3 ]; then + echo "usage: $0 CASE LIBRARY OUTPUT_DIR" >&2 + exit 2 +fi + +case_name="$1" +library="$2" +output_dir="$3" + +repo_root="$(cd "$(dirname "$0")/../../.." && pwd)" +source_file="$repo_root/bench/compile_time/cases/$case_name/$library.cc" +object_file="$output_dir/${case_name}_${library}.o" + +if [ ! -f "$source_file" ]; then + echo "unknown compile-time case: $case_name/$library" >&2 + exit 2 +fi + +mkdir -p "$output_dir" + +: "${CXX:=c++}" +: "${CXXFLAGS:=-std=c++20 -O2}" + +exec "$CXX" $CXXFLAGS \ + -I"$repo_root/include" \ + -I"$repo_root/bench/include" \ + -c "$source_file" \ + -o "$object_file" diff --git a/bench/compile_time/scripts/run.sh b/bench/compile_time/scripts/run.sh new file mode 100755 index 0000000..1f8449e --- /dev/null +++ b/bench/compile_time/scripts/run.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "$0")/../../.." && pwd)" +out_dir="${1:-$repo_root/bench/results/compile_time}" + +cases=(simple many_options subcommand) +libraries=(cli20 cli11 argparse cxxopts) + +mkdir -p "$out_dir" +result_file="$out_dir/results.tsv" +printf "case\tlibrary\tseconds\n" > "$result_file" + +for case_name in "${cases[@]}"; do + for library in "${libraries[@]}"; do + object_dir="$out_dir/objects" + rm -f "$object_dir/${case_name}_${library}.o" + start="$(python3 -c 'import time; print(time.perf_counter())')" + "$repo_root/bench/compile_time/scripts/compile-one.sh" \ + "$case_name" "$library" "$object_dir" + end="$(python3 -c 'import time; print(time.perf_counter())')" + elapsed="$(python3 -c 'import sys; print(f"{float(sys.argv[2]) - float(sys.argv[1]):.6f}")' "$start" "$end")" + printf "%s\t%s\t%s\n" "$case_name" "$library" "$elapsed" | tee -a "$result_file" + done +done diff --git a/bench/include/bench/allocation_counter.hh b/bench/include/bench/allocation_counter.hh new file mode 100644 index 0000000..08fd296 --- /dev/null +++ b/bench/include/bench/allocation_counter.hh @@ -0,0 +1,44 @@ +#pragma once + +#include +#include + +namespace bench { + +struct AllocationStats { + std::size_t allocations{}; + std::size_t bytes{}; +}; + +class AllocationCounter { + public: + static auto reset() -> void { + allocations().store(0, std::memory_order_relaxed); + bytes().store(0, std::memory_order_relaxed); + } + + static auto record(std::size_t n) -> void { + allocations().fetch_add(1, std::memory_order_relaxed); + bytes().fetch_add(n, std::memory_order_relaxed); + } + + [[nodiscard]] static auto snapshot() -> AllocationStats { + return { + .allocations = allocations().load(std::memory_order_relaxed), + .bytes = bytes().load(std::memory_order_relaxed), + }; + } + + private: + static auto allocations() -> std::atomic& { + static std::atomic value{}; + return value; + } + + static auto bytes() -> std::atomic& { + static std::atomic value{}; + return value; + } +}; + +} // namespace bench diff --git a/bench/include/bench/argv.hh b/bench/include/bench/argv.hh new file mode 100644 index 0000000..ffeaaec --- /dev/null +++ b/bench/include/bench/argv.hh @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include +#include + +namespace bench { + +struct Argv { + std::vector storage; + std::vector raw; + std::vector views; + + Argv(std::initializer_list args) { + storage.reserve(args.size()); + raw.reserve(args.size()); + views.reserve(args.size()); + for (std::string_view arg : args) { + storage.emplace_back(arg); + } + refresh(); + } + + auto refresh() -> void { + raw.clear(); + views.clear(); + for (auto& arg : storage) { + raw.push_back(arg.data()); + views.emplace_back(arg); + } + } + + [[nodiscard]] auto argc() const -> int { return static_cast(raw.size()); } + + [[nodiscard]] auto argv() -> char** { return raw.data(); } + + [[nodiscard]] auto span() const -> std::span { + return views; + } +}; + +} // namespace bench diff --git a/bench/results/.gitignore b/bench/results/.gitignore new file mode 100644 index 0000000..bf27f31 --- /dev/null +++ b/bench/results/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!.gitkeep diff --git a/bench/results/.gitkeep b/bench/results/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/bench/results/.gitkeep @@ -0,0 +1 @@ + diff --git a/bench/runtime/CMakeLists.txt b/bench/runtime/CMakeLists.txt new file mode 100644 index 0000000..c9f3fd5 --- /dev/null +++ b/bench/runtime/CMakeLists.txt @@ -0,0 +1,15 @@ +add_executable(bench_runtime + simple.cc + many_options.cc + subcommand.cc +) + +target_link_libraries(bench_runtime PRIVATE + bench_common + bench_warnings + bench_cli11 + bench_argparse + bench_cxxopts + benchmark::benchmark + benchmark::benchmark_main +) diff --git a/bench/runtime/many_options.cc b/bench/runtime/many_options.cc new file mode 100644 index 0000000..da2b7f4 --- /dev/null +++ b/bench/runtime/many_options.cc @@ -0,0 +1,144 @@ +#include + +#include +#include +#include +#include + +#include "bench/argv.hh" +#include "cli/argument.hh" +#include "cli/parser.hh" + +namespace { + +struct Cli20ManyOptions { + cli::StringOption<"opt01"> opt01; + cli::StringOption<"opt02"> opt02; + cli::StringOption<"opt03"> opt03; + cli::StringOption<"opt04"> opt04; + cli::StringOption<"opt05"> opt05; + cli::StringOption<"opt06"> opt06; + cli::StringOption<"opt07"> opt07; + cli::StringOption<"opt08"> opt08; + cli::StringOption<"opt09"> opt09; + cli::StringOption<"opt10"> opt10; + cli::StringOption<"opt11"> opt11; + cli::StringOption<"opt12"> opt12; + cli::StringOption<"opt13"> opt13; + cli::StringOption<"opt14"> opt14; + cli::StringOption<"opt15"> opt15; + cli::StringOption<"opt16"> opt16; +}; + +auto many_argv() -> bench::Argv& { + static bench::Argv args{"prog", "--opt01", "v01", "--opt02", "v02", "--opt03", + "v03", "--opt04", "v04", "--opt05", "v05", "--opt06", + "v06", "--opt07", "v07", "--opt08", "v08", "--opt09", + "v09", "--opt10", "v10", "--opt11", "v11", "--opt12", + "v12", "--opt13", "v13", "--opt14", "v14", "--opt15", + "v15", "--opt16", "v16"}; + return args; +} + +void add_cli11_options(CLI::App& app, std::string (&values)[16]) { + app.add_option("--opt01", values[0]); + app.add_option("--opt02", values[1]); + app.add_option("--opt03", values[2]); + app.add_option("--opt04", values[3]); + app.add_option("--opt05", values[4]); + app.add_option("--opt06", values[5]); + app.add_option("--opt07", values[6]); + app.add_option("--opt08", values[7]); + app.add_option("--opt09", values[8]); + app.add_option("--opt10", values[9]); + app.add_option("--opt11", values[10]); + app.add_option("--opt12", values[11]); + app.add_option("--opt13", values[12]); + app.add_option("--opt14", values[13]); + app.add_option("--opt15", values[14]); + app.add_option("--opt16", values[15]); +} + +void add_argparse_options(argparse::ArgumentParser& program) { + program.add_argument("--opt01"); + program.add_argument("--opt02"); + program.add_argument("--opt03"); + program.add_argument("--opt04"); + program.add_argument("--opt05"); + program.add_argument("--opt06"); + program.add_argument("--opt07"); + program.add_argument("--opt08"); + program.add_argument("--opt09"); + program.add_argument("--opt10"); + program.add_argument("--opt11"); + program.add_argument("--opt12"); + program.add_argument("--opt13"); + program.add_argument("--opt14"); + program.add_argument("--opt15"); + program.add_argument("--opt16"); +} + +void add_cxxopts_options(cxxopts::Options& options) { + options.add_options()("opt01", "opt01", cxxopts::value())( + "opt02", "opt02", cxxopts::value())( + "opt03", "opt03", cxxopts::value())( + "opt04", "opt04", cxxopts::value())( + "opt05", "opt05", cxxopts::value())( + "opt06", "opt06", cxxopts::value())( + "opt07", "opt07", cxxopts::value())( + "opt08", "opt08", cxxopts::value())( + "opt09", "opt09", cxxopts::value())( + "opt10", "opt10", cxxopts::value())( + "opt11", "opt11", cxxopts::value())( + "opt12", "opt12", cxxopts::value())( + "opt13", "opt13", cxxopts::value())( + "opt14", "opt14", cxxopts::value())( + "opt15", "opt15", cxxopts::value())( + "opt16", "opt16", cxxopts::value()); +} + +void BM_Cli20ManyOptions(benchmark::State& state) { + auto& args = many_argv(); + for (auto _ : state) { + auto result = cli::Parser{}.parse(args.span(), 1); + benchmark::DoNotOptimize(result); + } +} + +void BM_Cli11ManyOptions(benchmark::State& state) { + auto& args = many_argv(); + for (auto _ : state) { + CLI::App app{"many_options"}; + std::string values[16]; + add_cli11_options(app, values); + app.parse(args.argc(), args.argv()); + benchmark::DoNotOptimize(values); + } +} + +void BM_ArgparseManyOptions(benchmark::State& state) { + auto& args = many_argv(); + for (auto _ : state) { + argparse::ArgumentParser program{"many_options"}; + add_argparse_options(program); + program.parse_args(args.argc(), args.argv()); + benchmark::DoNotOptimize(program); + } +} + +void BM_CxxoptsManyOptions(benchmark::State& state) { + auto& args = many_argv(); + for (auto _ : state) { + cxxopts::Options options{"many_options"}; + add_cxxopts_options(options); + auto result = options.parse(args.argc(), args.argv()); + benchmark::DoNotOptimize(result); + } +} + +BENCHMARK(BM_Cli20ManyOptions); +BENCHMARK(BM_Cli11ManyOptions); +BENCHMARK(BM_ArgparseManyOptions); +BENCHMARK(BM_CxxoptsManyOptions); + +} // namespace diff --git a/bench/runtime/simple.cc b/bench/runtime/simple.cc new file mode 100644 index 0000000..bde3bb3 --- /dev/null +++ b/bench/runtime/simple.cc @@ -0,0 +1,81 @@ +#include + +#include +#include +#include +#include + +#include "bench/argv.hh" +#include "cli/argument.hh" +#include "cli/parser.hh" + +namespace { + +struct Cli20Simple { + cli::Flag<"verbose", 'v'> verbose; + cli::StringOption<"output", 'o'> output; + cli::Positional input{{.presence = cli::required}}; +}; + +auto simple_argv() -> bench::Argv& { + static bench::Argv args{"prog", "-v", "-o", "out.txt", "input.txt"}; + return args; +} + +void BM_Cli20Simple(benchmark::State& state) { + auto& args = simple_argv(); + for (auto _ : state) { + auto result = cli::Parser{}.parse(args.span(), 1); + benchmark::DoNotOptimize(result); + } +} + +void BM_Cli11Simple(benchmark::State& state) { + auto& args = simple_argv(); + for (auto _ : state) { + CLI::App app{"simple"}; + bool verbose = false; + std::string output; + std::string input; + app.add_flag("-v,--verbose", verbose); + app.add_option("-o,--output", output); + app.add_option("input", input)->required(); + app.parse(args.argc(), args.argv()); + benchmark::DoNotOptimize(verbose); + benchmark::DoNotOptimize(output); + benchmark::DoNotOptimize(input); + } +} + +void BM_ArgparseSimple(benchmark::State& state) { + auto& args = simple_argv(); + for (auto _ : state) { + argparse::ArgumentParser program{"simple"}; + program.add_argument("-v", "--verbose").flag(); + program.add_argument("-o", "--output"); + program.add_argument("input"); + program.parse_args(args.argc(), args.argv()); + benchmark::DoNotOptimize(program); + } +} + +void BM_CxxoptsSimple(benchmark::State& state) { + auto& args = simple_argv(); + for (auto _ : state) { + cxxopts::Options options{"simple"}; + options.add_options()("v,verbose", "verbose", + cxxopts::value()->default_value("false"))( + "o,output", "output", cxxopts::value())( + "input", "input", cxxopts::value()); + options.parse_positional({"input"}); + auto result = options.parse(args.argc(), args.argv()); + benchmark::DoNotOptimize(result); + } +} + +BENCHMARK(BM_Cli20Simple); +BENCHMARK(BM_Cli11Simple); +BENCHMARK(BM_ArgparseSimple); +BENCHMARK(BM_CxxoptsSimple); + +} // namespace diff --git a/bench/runtime/subcommand.cc b/bench/runtime/subcommand.cc new file mode 100644 index 0000000..2b12747 --- /dev/null +++ b/bench/runtime/subcommand.cc @@ -0,0 +1,89 @@ +#include + +#include +#include +#include +#include + +#include "bench/argv.hh" +#include "cli/argument.hh" +#include "cli/parser.hh" + +namespace { + +struct Cli20Build { + cli::StringOption<"profile"> profile; +}; + +struct Cli20Test { + cli::Flag<"verbose", 'v'> verbose; +}; + +struct Cli20Subcommand { + cli::Command<"build", Cli20Build> build; + cli::Command<"test", Cli20Test> test; +}; + +auto subcommand_argv() -> bench::Argv& { + static bench::Argv args{"prog", "build", "--profile", "release"}; + return args; +} + +void BM_Cli20Subcommand(benchmark::State& state) { + auto& args = subcommand_argv(); + for (auto _ : state) { + auto result = cli::Parser{}.parse(args.span(), 1); + benchmark::DoNotOptimize(result); + } +} + +void BM_Cli11Subcommand(benchmark::State& state) { + auto& args = subcommand_argv(); + for (auto _ : state) { + CLI::App app{"subcommand"}; + std::string profile; + bool verbose = false; + auto* build = app.add_subcommand("build"); + build->add_option("--profile", profile); + auto* test = app.add_subcommand("test"); + test->add_flag("-v,--verbose", verbose); + app.parse(args.argc(), args.argv()); + benchmark::DoNotOptimize(profile); + benchmark::DoNotOptimize(verbose); + } +} + +void BM_ArgparseSubcommand(benchmark::State& state) { + auto& args = subcommand_argv(); + for (auto _ : state) { + argparse::ArgumentParser program{"subcommand"}; + argparse::ArgumentParser build{"build"}; + build.add_argument("--profile"); + argparse::ArgumentParser test{"test"}; + test.add_argument("-v", "--verbose").flag(); + program.add_subparser(build); + program.add_subparser(test); + program.parse_args(args.argc(), args.argv()); + benchmark::DoNotOptimize(program); + } +} + +void BM_CxxoptsSubcommand(benchmark::State& state) { + auto& args = subcommand_argv(); + for (auto _ : state) { + const std::string command = args.storage[1]; + if (command == "build") { + cxxopts::Options options{"build"}; + options.add_options()("profile", "profile", cxxopts::value()); + auto result = options.parse(args.argc() - 1, args.argv() + 1); + benchmark::DoNotOptimize(result); + } + } +} + +BENCHMARK(BM_Cli20Subcommand); +BENCHMARK(BM_Cli11Subcommand); +BENCHMARK(BM_ArgparseSubcommand); +BENCHMARK(BM_CxxoptsSubcommand); + +} // namespace diff --git a/bench/scripts/run-runtime.sh b/bench/scripts/run-runtime.sh new file mode 100755 index 0000000..ee72547 --- /dev/null +++ b/bench/scripts/run-runtime.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "$0")/../.." && pwd)" +build_dir="${BUILD_DIR:-$repo_root/build-bench}" +results_dir="$repo_root/bench/results" + +cmake -S "$repo_root/bench" -B "$build_dir" -G Ninja -DCMAKE_BUILD_TYPE=Release +cmake --build "$build_dir" --target bench_runtime + +mkdir -p "$results_dir" +"$build_dir/runtime/bench_runtime" \ + --benchmark_out="$results_dir/runtime.json" \ + --benchmark_out_format=json \ + "$@" diff --git a/bench/scripts/run-size.sh b/bench/scripts/run-size.sh new file mode 100755 index 0000000..afb7077 --- /dev/null +++ b/bench/scripts/run-size.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "$0")/../.." && pwd)" +build_dir="${BUILD_DIR:-$repo_root/build-bench}" +results_dir="$repo_root/bench/results" + +cmake -S "$repo_root/bench" -B "$build_dir" -G Ninja -DCMAKE_BUILD_TYPE=Release +cmake --build "$build_dir" --target all + +mkdir -p "$results_dir" +result_file="$results_dir/size.tsv" +printf "case\tlibrary\tbytes\n" > "$result_file" + +for binary in "$build_dir"/size/bench_size_*; do + [ -x "$binary" ] || continue + name="$(basename "$binary")" + rest="${name#bench_size_}" + library="${rest##*_}" + case_name="${rest%_$library}" + bytes="$(wc -c < "$binary" | tr -d ' ')" + printf "%s\t%s\t%s\n" "$case_name" "$library" "$bytes" | tee -a "$result_file" +done diff --git a/bench/scripts/summarize.py b/bench/scripts/summarize.py new file mode 100755 index 0000000..ca77c4d --- /dev/null +++ b/bench/scripts/summarize.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import csv +import json +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] +RESULTS = ROOT / "bench" / "results" + + +def print_tsv(path: Path, title: str) -> None: + if not path.exists(): + return + print(f"\n## {title}") + with path.open(newline="") as f: + rows = list(csv.reader(f, delimiter="\t")) + if not rows: + return + widths = [max(len(row[i]) for row in rows) for i in range(len(rows[0]))] + for row in rows: + print(" ".join(cell.ljust(widths[i]) for i, cell in enumerate(row))) + + +def print_runtime(path: Path) -> None: + if not path.exists(): + return + print("\n## Runtime") + data = json.loads(path.read_text()) + rows = [("name", "time_ns", "cpu_ns")] + for bench in data.get("benchmarks", []): + rows.append( + ( + bench.get("name", ""), + f"{bench.get('real_time', 0):.2f}", + f"{bench.get('cpu_time', 0):.2f}", + ) + ) + widths = [max(len(row[i]) for row in rows) for i in range(len(rows[0]))] + for row in rows: + print(" ".join(cell.ljust(widths[i]) for i, cell in enumerate(row))) + + +def main() -> None: + print_runtime(RESULTS / "runtime.json") + print_tsv(RESULTS / "size.tsv", "Binary Size") + print_tsv(RESULTS / "compile_time" / "results.tsv", "Compile Time") + + +if __name__ == "__main__": + main() diff --git a/bench/size/CMakeLists.txt b/bench/size/CMakeLists.txt new file mode 100644 index 0000000..90a8d2d --- /dev/null +++ b/bench/size/CMakeLists.txt @@ -0,0 +1,19 @@ +set(BENCH_SIZE_CASES simple many_options subcommand) +set(BENCH_SIZE_LIBS cli20 cli11 argparse cxxopts) + +foreach(case_name IN LISTS BENCH_SIZE_CASES) + foreach(lib_name IN LISTS BENCH_SIZE_LIBS) + set(target_name "bench_size_${case_name}_${lib_name}") + set(source_file "${case_name}/${lib_name}.cc") + if(lib_name STREQUAL "cli20") + set(link_target bench_common) + elseif(lib_name STREQUAL "cli11") + set(link_target bench_cli11) + elseif(lib_name STREQUAL "argparse") + set(link_target bench_argparse) + elseif(lib_name STREQUAL "cxxopts") + set(link_target bench_cxxopts) + endif() + bench_add_size_executable("${target_name}" "${source_file}" "${link_target}") + endforeach() +endforeach() diff --git a/bench/size/many_options/argparse.cc b/bench/size/many_options/argparse.cc new file mode 100644 index 0000000..33b4977 --- /dev/null +++ b/bench/size/many_options/argparse.cc @@ -0,0 +1,27 @@ +#include + +auto main(int argc, char** argv) -> int { + argparse::ArgumentParser program{"many_options"}; + program.add_argument("--opt01"); + program.add_argument("--opt02"); + program.add_argument("--opt03"); + program.add_argument("--opt04"); + program.add_argument("--opt05"); + program.add_argument("--opt06"); + program.add_argument("--opt07"); + program.add_argument("--opt08"); + program.add_argument("--opt09"); + program.add_argument("--opt10"); + program.add_argument("--opt11"); + program.add_argument("--opt12"); + program.add_argument("--opt13"); + program.add_argument("--opt14"); + program.add_argument("--opt15"); + program.add_argument("--opt16"); + try { + program.parse_args(argc, argv); + } catch (const std::runtime_error&) { + return 1; + } + return 0; +} diff --git a/bench/size/many_options/cli11.cc b/bench/size/many_options/cli11.cc new file mode 100644 index 0000000..bb3f547 --- /dev/null +++ b/bench/size/many_options/cli11.cc @@ -0,0 +1,29 @@ +#include +#include + +auto main(int argc, char** argv) -> int { + CLI::App app{"many_options"}; + std::string values[16]; + app.add_option("--opt01", values[0]); + app.add_option("--opt02", values[1]); + app.add_option("--opt03", values[2]); + app.add_option("--opt04", values[3]); + app.add_option("--opt05", values[4]); + app.add_option("--opt06", values[5]); + app.add_option("--opt07", values[6]); + app.add_option("--opt08", values[7]); + app.add_option("--opt09", values[8]); + app.add_option("--opt10", values[9]); + app.add_option("--opt11", values[10]); + app.add_option("--opt12", values[11]); + app.add_option("--opt13", values[12]); + app.add_option("--opt14", values[13]); + app.add_option("--opt15", values[14]); + app.add_option("--opt16", values[15]); + try { + app.parse(argc, argv); + } catch (const CLI::ParseError& e) { + return app.exit(e); + } + return 0; +} diff --git a/bench/size/many_options/cli20.cc b/bench/size/many_options/cli20.cc new file mode 100644 index 0000000..80c5861 --- /dev/null +++ b/bench/size/many_options/cli20.cc @@ -0,0 +1,26 @@ +#include "cli/argument.hh" +#include "cli/parser.hh" + +struct Args { + cli::StringOption<"opt01"> opt01; + cli::StringOption<"opt02"> opt02; + cli::StringOption<"opt03"> opt03; + cli::StringOption<"opt04"> opt04; + cli::StringOption<"opt05"> opt05; + cli::StringOption<"opt06"> opt06; + cli::StringOption<"opt07"> opt07; + cli::StringOption<"opt08"> opt08; + cli::StringOption<"opt09"> opt09; + cli::StringOption<"opt10"> opt10; + cli::StringOption<"opt11"> opt11; + cli::StringOption<"opt12"> opt12; + cli::StringOption<"opt13"> opt13; + cli::StringOption<"opt14"> opt14; + cli::StringOption<"opt15"> opt15; + cli::StringOption<"opt16"> opt16; +}; + +auto main(int argc, char** argv) -> int { + auto parsed = cli::Parser{}.parse(argc, argv); + return parsed.has_value() ? 0 : 1; +} diff --git a/bench/size/many_options/cxxopts.cc b/bench/size/many_options/cxxopts.cc new file mode 100644 index 0000000..b0a3e65 --- /dev/null +++ b/bench/size/many_options/cxxopts.cc @@ -0,0 +1,28 @@ +#include +#include + +auto main(int argc, char** argv) -> int { + cxxopts::Options options{"many_options"}; + options.add_options()("opt01", "opt01", cxxopts::value())( + "opt02", "opt02", cxxopts::value())( + "opt03", "opt03", cxxopts::value())( + "opt04", "opt04", cxxopts::value())( + "opt05", "opt05", cxxopts::value())( + "opt06", "opt06", cxxopts::value())( + "opt07", "opt07", cxxopts::value())( + "opt08", "opt08", cxxopts::value())( + "opt09", "opt09", cxxopts::value())( + "opt10", "opt10", cxxopts::value())( + "opt11", "opt11", cxxopts::value())( + "opt12", "opt12", cxxopts::value())( + "opt13", "opt13", cxxopts::value())( + "opt14", "opt14", cxxopts::value())( + "opt15", "opt15", cxxopts::value())( + "opt16", "opt16", cxxopts::value()); + try { + (void)options.parse(argc, argv); + } catch (const cxxopts::exceptions::exception&) { + return 1; + } + return 0; +} diff --git a/bench/size/simple/argparse.cc b/bench/size/simple/argparse.cc new file mode 100644 index 0000000..6414088 --- /dev/null +++ b/bench/size/simple/argparse.cc @@ -0,0 +1,14 @@ +#include + +auto main(int argc, char** argv) -> int { + argparse::ArgumentParser program{"simple"}; + program.add_argument("-v", "--verbose").flag(); + program.add_argument("-o", "--output"); + program.add_argument("input"); + try { + program.parse_args(argc, argv); + } catch (const std::runtime_error&) { + return 1; + } + return 0; +} diff --git a/bench/size/simple/cli11.cc b/bench/size/simple/cli11.cc new file mode 100644 index 0000000..d94e2e4 --- /dev/null +++ b/bench/size/simple/cli11.cc @@ -0,0 +1,18 @@ +#include +#include + +auto main(int argc, char** argv) -> int { + CLI::App app{"simple"}; + bool verbose = false; + std::string output; + std::string input; + app.add_flag("-v,--verbose", verbose); + app.add_option("-o,--output", output); + app.add_option("input", input)->required(); + try { + app.parse(argc, argv); + } catch (const CLI::ParseError& e) { + return app.exit(e); + } + return 0; +} diff --git a/bench/size/simple/cli20.cc b/bench/size/simple/cli20.cc new file mode 100644 index 0000000..1d75099 --- /dev/null +++ b/bench/size/simple/cli20.cc @@ -0,0 +1,15 @@ +#include + +#include "cli/argument.hh" +#include "cli/parser.hh" + +struct Args { + cli::Flag<"verbose", 'v'> verbose; + cli::StringOption<"output", 'o'> output; + cli::Positional input{{.presence = cli::required}}; +}; + +auto main(int argc, char** argv) -> int { + auto parsed = cli::Parser{}.parse(argc, argv); + return parsed.has_value() ? 0 : 1; +} diff --git a/bench/size/simple/cxxopts.cc b/bench/size/simple/cxxopts.cc new file mode 100644 index 0000000..e11266a --- /dev/null +++ b/bench/size/simple/cxxopts.cc @@ -0,0 +1,17 @@ +#include +#include + +auto main(int argc, char** argv) -> int { + cxxopts::Options options{"simple"}; + options.add_options()("v,verbose", "verbose", + cxxopts::value()->default_value("false"))( + "o,output", "output", cxxopts::value())( + "input", "input", cxxopts::value()); + options.parse_positional({"input"}); + try { + (void)options.parse(argc, argv); + } catch (const cxxopts::exceptions::exception&) { + return 1; + } + return 0; +} diff --git a/bench/size/subcommand/argparse.cc b/bench/size/subcommand/argparse.cc new file mode 100644 index 0000000..b665797 --- /dev/null +++ b/bench/size/subcommand/argparse.cc @@ -0,0 +1,17 @@ +#include + +auto main(int argc, char** argv) -> int { + argparse::ArgumentParser program{"subcommand"}; + argparse::ArgumentParser build{"build"}; + build.add_argument("--profile"); + argparse::ArgumentParser test{"test"}; + test.add_argument("-v", "--verbose").flag(); + program.add_subparser(build); + program.add_subparser(test); + try { + program.parse_args(argc, argv); + } catch (const std::runtime_error&) { + return 1; + } + return 0; +} diff --git a/bench/size/subcommand/cli11.cc b/bench/size/subcommand/cli11.cc new file mode 100644 index 0000000..4a73dff --- /dev/null +++ b/bench/size/subcommand/cli11.cc @@ -0,0 +1,18 @@ +#include +#include + +auto main(int argc, char** argv) -> int { + CLI::App app{"subcommand"}; + std::string profile; + bool verbose = false; + auto* build = app.add_subcommand("build"); + build->add_option("--profile", profile); + auto* test = app.add_subcommand("test"); + test->add_flag("-v,--verbose", verbose); + try { + app.parse(argc, argv); + } catch (const CLI::ParseError& e) { + return app.exit(e); + } + return 0; +} diff --git a/bench/size/subcommand/cli20.cc b/bench/size/subcommand/cli20.cc new file mode 100644 index 0000000..16c03dd --- /dev/null +++ b/bench/size/subcommand/cli20.cc @@ -0,0 +1,20 @@ +#include "cli/argument.hh" +#include "cli/parser.hh" + +struct BuildArgs { + cli::StringOption<"profile"> profile; +}; + +struct TestArgs { + cli::Flag<"verbose", 'v'> verbose; +}; + +struct Args { + cli::Command<"build", BuildArgs> build; + cli::Command<"test", TestArgs> test; +}; + +auto main(int argc, char** argv) -> int { + auto parsed = cli::Parser{}.parse(argc, argv); + return parsed.has_value() ? 0 : 1; +} diff --git a/bench/size/subcommand/cxxopts.cc b/bench/size/subcommand/cxxopts.cc new file mode 100644 index 0000000..cb0c734 --- /dev/null +++ b/bench/size/subcommand/cxxopts.cc @@ -0,0 +1,25 @@ +#include +#include + +auto main(int argc, char** argv) -> int { + if (argc < 2) return 1; + const std::string command = argv[1]; + try { + if (command == "build") { + cxxopts::Options options{"build"}; + options.add_options()("profile", "profile", cxxopts::value()); + (void)options.parse(argc - 1, argv + 1); + return 0; + } + if (command == "test") { + cxxopts::Options options{"test"}; + options.add_options()("v,verbose", "verbose", + cxxopts::value()->default_value("false")); + (void)options.parse(argc - 1, argv + 1); + return 0; + } + } catch (const cxxopts::exceptions::exception&) { + return 1; + } + return 1; +} diff --git a/flake.nix b/flake.nix index 518f6ef..10596ea 100644 --- a/flake.nix +++ b/flake.nix @@ -30,6 +30,34 @@ { pkgs, config, ... }: let version = builtins.replaceStrings [ "\n" ] [ "" ] (builtins.readFile ./VERSION); + google_bench = pkgs.stdenv.mkDerivation rec { + pname = "google-benchmark"; + version = "1.9.5"; + + src = pkgs.fetchFromGitHub { + owner = "google"; + repo = "benchmark"; + rev = "v${version}"; + hash = "sha256-Mm4pG7zMB00iof32CxreoNBFnduPZTMp3reHMCIAFPQ="; + }; + + nativeBuildInputs = [ + pkgs.cmake + ]; + + cmakeFlags = [ + "-DBENCHMARK_ENABLE_TESTING=OFF" + "-DBENCHMARK_ENABLE_GTEST_TESTS=OFF" + "-DBENCHMARK_ENABLE_INSTALL=ON" + ]; + + meta = { + description = "A microbenchmark support library"; + homepage = "https://github.com/google/benchmark"; + license = pkgs.lib.licenses.asl20; + platforms = pkgs.lib.platforms.unix; + }; + }; in rec { treefmt = { @@ -52,7 +80,14 @@ pkgs.clang-tools pkgs.doxygen pkgs.emscripten + pkgs.boost + pkgs.cxxopts + pkgs.cli11 + pkgs.argparse + google_bench config.treefmt.build.wrapper + pkgs.hyperfine + pkgs.python3 ]; }; From 8e405e574481eea4ff282b53884705122bfafa80 Mon Sep 17 00:00:00 2001 From: gen740 <54583542+gen740@users.noreply.github.com> Date: Tue, 12 May 2026 20:55:06 +0900 Subject: [PATCH 02/15] Extend benchmark reporting --- bench/CMakeLists.txt | 5 +- bench/compile_time/scripts/compile-one.sh | 3 +- bench/compile_time/scripts/run.sh | 30 ++- bench/include/bench/allocation_counter.hh | 34 ++++ bench/include/bench/many_options.hh | 75 ++++++++ bench/runtime/CMakeLists.txt | 1 + bench/runtime/allocation_count.cc | 100 ++++++++++ bench/runtime/many_options.cc | 220 +++++++++++++--------- bench/runtime/simple.cc | 83 +++++--- bench/runtime/subcommand.cc | 86 ++++++--- bench/scripts/run-runtime.sh | 11 +- bench/scripts/run-size.sh | 27 ++- bench/scripts/summarize.py | 104 ++++++---- bench/size/many_options/argparse.cc | 37 ++-- bench/size/many_options/cli11.cc | 45 +++-- bench/size/many_options/cli20.cc | 37 ++-- bench/size/many_options/cxxopts.cc | 35 ++-- flake.nix | 49 +++++ 18 files changed, 720 insertions(+), 262 deletions(-) create mode 100644 bench/include/bench/many_options.hh create mode 100644 bench/runtime/allocation_count.cc diff --git a/bench/CMakeLists.txt b/bench/CMakeLists.txt index 4a51353..5b0ed8b 100644 --- a/bench/CMakeLists.txt +++ b/bench/CMakeLists.txt @@ -2,7 +2,10 @@ cmake_minimum_required(VERSION 3.20) project(cli20_bench LANGUAGES CXX) -set(CMAKE_CXX_STANDARD 20) +set(BENCH_CXX_STANDARD "20" CACHE STRING "C++ standard used for benchmark builds") +set_property(CACHE BENCH_CXX_STANDARD PROPERTY STRINGS 20 26) + +set(CMAKE_CXX_STANDARD "${BENCH_CXX_STANDARD}") set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) diff --git a/bench/compile_time/scripts/compile-one.sh b/bench/compile_time/scripts/compile-one.sh index 4ae2d6c..fcbea53 100755 --- a/bench/compile_time/scripts/compile-one.sh +++ b/bench/compile_time/scripts/compile-one.sh @@ -22,7 +22,8 @@ fi mkdir -p "$output_dir" : "${CXX:=c++}" -: "${CXXFLAGS:=-std=c++20 -O2}" +: "${BENCH_CXX_STANDARD:=20}" +: "${CXXFLAGS:=-std=c++$BENCH_CXX_STANDARD -O2}" exec "$CXX" $CXXFLAGS \ -I"$repo_root/include" \ diff --git a/bench/compile_time/scripts/run.sh b/bench/compile_time/scripts/run.sh index 1f8449e..4819136 100755 --- a/bench/compile_time/scripts/run.sh +++ b/bench/compile_time/scripts/run.sh @@ -2,24 +2,34 @@ set -euo pipefail repo_root="$(cd "$(dirname "$0")/../../.." && pwd)" -out_dir="${1:-$repo_root/bench/results/compile_time}" +cxx_standard="${BENCH_CXX_STANDARD:-20}" +out_dir="${1:-$repo_root/bench/results/compile_time/cxx$cxx_standard}" +hyperfine_runs="${HYPERFINE_RUNS:-5}" +hyperfine_warmup="${HYPERFINE_WARMUP:-1}" cases=(simple many_options subcommand) libraries=(cli20 cli11 argparse cxxopts) mkdir -p "$out_dir" result_file="$out_dir/results.tsv" -printf "case\tlibrary\tseconds\n" > "$result_file" +json_dir="$out_dir/json" +object_dir="$out_dir/objects" +printf "standard\tcase\tlibrary\tseconds\n" > "$result_file" for case_name in "${cases[@]}"; do for library in "${libraries[@]}"; do - object_dir="$out_dir/objects" - rm -f "$object_dir/${case_name}_${library}.o" - start="$(python3 -c 'import time; print(time.perf_counter())')" - "$repo_root/bench/compile_time/scripts/compile-one.sh" \ - "$case_name" "$library" "$object_dir" - end="$(python3 -c 'import time; print(time.perf_counter())')" - elapsed="$(python3 -c 'import sys; print(f"{float(sys.argv[2]) - float(sys.argv[1]):.6f}")' "$start" "$end")" - printf "%s\t%s\t%s\n" "$case_name" "$library" "$elapsed" | tee -a "$result_file" + mkdir -p "$json_dir" "$object_dir" + object_file="$object_dir/${case_name}_${library}.o" + json_file="$json_dir/${case_name}_${library}.json" + hyperfine \ + --warmup "$hyperfine_warmup" \ + --runs "$hyperfine_runs" \ + --prepare "rm -f '$object_file'" \ + --export-json "$json_file" \ + "$repo_root/bench/compile_time/scripts/compile-one.sh '$case_name' '$library' '$object_dir'" \ + >/dev/null + elapsed="$(python3 -c 'import json, sys; print("{:.6f}".format(json.load(open(sys.argv[1]))["results"][0]["mean"]))' "$json_file")" + printf "c++%s\t%s\t%s\t%s\n" "$cxx_standard" "$case_name" "$library" "$elapsed" | + tee -a "$result_file" done done diff --git a/bench/include/bench/allocation_counter.hh b/bench/include/bench/allocation_counter.hh index 08fd296..3fb6ba7 100644 --- a/bench/include/bench/allocation_counter.hh +++ b/bench/include/bench/allocation_counter.hh @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -12,12 +14,23 @@ struct AllocationStats { class AllocationCounter { public: + AllocationCounter() { + reset(); + enabled() = true; + } + + AllocationCounter(const AllocationCounter&) = delete; + auto operator=(const AllocationCounter&) -> AllocationCounter& = delete; + + ~AllocationCounter() { enabled() = false; } + static auto reset() -> void { allocations().store(0, std::memory_order_relaxed); bytes().store(0, std::memory_order_relaxed); } static auto record(std::size_t n) -> void { + if (!enabled()) return; allocations().fetch_add(1, std::memory_order_relaxed); bytes().fetch_add(n, std::memory_order_relaxed); } @@ -30,6 +43,11 @@ class AllocationCounter { } private: + static auto enabled() -> bool& { + static thread_local bool value = false; + return value; + } + static auto allocations() -> std::atomic& { static std::atomic value{}; return value; @@ -41,4 +59,20 @@ class AllocationCounter { } }; +inline auto set_allocation_counters(benchmark::State& state, + std::size_t allocations, std::size_t bytes) + -> void { + state.counters["allocs"] = benchmark::Counter( + static_cast(allocations), benchmark::Counter::kAvgIterations); + state.counters["bytes"] = benchmark::Counter( + static_cast(bytes), benchmark::Counter::kAvgIterations); +} + +template +auto measure_allocations(Fn&& fn) -> AllocationStats { + AllocationCounter counter; + fn(); + return counter.snapshot(); +} + } // namespace bench diff --git a/bench/include/bench/many_options.hh b/bench/include/bench/many_options.hh new file mode 100644 index 0000000..7d2fe63 --- /dev/null +++ b/bench/include/bench/many_options.hh @@ -0,0 +1,75 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace bench { + +enum class Mode { fast, balanced, precise }; + +inline auto mode_name(Mode mode) -> std::string_view { + switch (mode) { + case Mode::fast: + return "fast"; + case Mode::balanced: + return "balanced"; + case Mode::precise: + return "precise"; + } + return "unknown"; +} + +inline auto parse_mode(std::string_view value) -> std::optional { + if (value == "fast") return Mode::fast; + if (value == "balanced") return Mode::balanced; + if (value == "precise") return Mode::precise; + return std::nullopt; +} + +inline auto parse_mode_or_throw(const std::string& value) -> Mode { + if (auto mode = parse_mode(value)) return *mode; + throw std::runtime_error("invalid mode: " + value); +} + +inline auto operator>>(std::istream& in, Mode& mode) -> std::istream& { + std::string value; + in >> value; + if (auto parsed = parse_mode(value)) { + mode = *parsed; + } else { + in.setstate(std::ios::failbit); + } + return in; +} + +struct ManyValues { + std::string name; + std::string output; + std::string target; + std::string define; + int jobs{}; + int retries{}; + int port{}; + int depth{}; + double ratio{}; + double timeout{}; + double threshold{}; + double scale{}; + std::filesystem::path config; + std::filesystem::path cache; + Mode mode{}; + Mode color{}; +}; + +} // namespace bench + +template <> +struct std::formatter : std::formatter { + auto format(bench::Mode mode, std::format_context& ctx) const { + return std::formatter::format(bench::mode_name(mode), ctx); + } +}; diff --git a/bench/runtime/CMakeLists.txt b/bench/runtime/CMakeLists.txt index c9f3fd5..519e64c 100644 --- a/bench/runtime/CMakeLists.txt +++ b/bench/runtime/CMakeLists.txt @@ -1,4 +1,5 @@ add_executable(bench_runtime + allocation_count.cc simple.cc many_options.cc subcommand.cc diff --git a/bench/runtime/allocation_count.cc b/bench/runtime/allocation_count.cc new file mode 100644 index 0000000..2c131ae --- /dev/null +++ b/bench/runtime/allocation_count.cc @@ -0,0 +1,100 @@ +#include +#include + +#include "bench/allocation_counter.hh" + +namespace { + +auto allocate(std::size_t size) -> void* { + if (void* ptr = std::malloc(size == 0 ? 1 : size)) { + bench::AllocationCounter::record(size); + return ptr; + } + throw std::bad_alloc{}; +} + +auto allocate_aligned(std::size_t size, std::align_val_t alignment) -> void* { + void* ptr = nullptr; + const auto align = static_cast(alignment); + if (posix_memalign(&ptr, align, size == 0 ? align : size) == 0) { + bench::AllocationCounter::record(size); + return ptr; + } + throw std::bad_alloc{}; +} + +} // namespace + +auto operator new(std::size_t size) -> void* { return allocate(size); } + +auto operator new[](std::size_t size) -> void* { return allocate(size); } + +auto operator new(std::size_t size, const std::nothrow_t&) noexcept -> void* { + try { + return allocate(size); + } catch (...) { + return nullptr; + } +} + +auto operator new[](std::size_t size, const std::nothrow_t&) noexcept -> void* { + try { + return allocate(size); + } catch (...) { + return nullptr; + } +} + +auto operator new(std::size_t size, std::align_val_t alignment) -> void* { + return allocate_aligned(size, alignment); +} + +auto operator new[](std::size_t size, std::align_val_t alignment) -> void* { + return allocate_aligned(size, alignment); +} + +auto operator new(std::size_t size, std::align_val_t alignment, + const std::nothrow_t&) noexcept -> void* { + try { + return allocate_aligned(size, alignment); + } catch (...) { + return nullptr; + } +} + +auto operator new[](std::size_t size, std::align_val_t alignment, + const std::nothrow_t&) noexcept -> void* { + try { + return allocate_aligned(size, alignment); + } catch (...) { + return nullptr; + } +} + +void operator delete(void* ptr) noexcept { std::free(ptr); } + +void operator delete[](void* ptr) noexcept { std::free(ptr); } + +void operator delete(void* ptr, std::size_t) noexcept { std::free(ptr); } + +void operator delete[](void* ptr, std::size_t) noexcept { std::free(ptr); } + +void operator delete(void* ptr, const std::nothrow_t&) noexcept { + std::free(ptr); +} + +void operator delete[](void* ptr, const std::nothrow_t&) noexcept { + std::free(ptr); +} + +void operator delete(void* ptr, std::align_val_t) noexcept { std::free(ptr); } + +void operator delete[](void* ptr, std::align_val_t) noexcept { std::free(ptr); } + +void operator delete(void* ptr, std::size_t, std::align_val_t) noexcept { + std::free(ptr); +} + +void operator delete[](void* ptr, std::size_t, std::align_val_t) noexcept { + std::free(ptr); +} diff --git a/bench/runtime/many_options.cc b/bench/runtime/many_options.cc index da2b7f4..c821b3b 100644 --- a/bench/runtime/many_options.cc +++ b/bench/runtime/many_options.cc @@ -1,10 +1,14 @@ +#include "bench/many_options.hh" + #include #include #include #include +#include #include +#include "bench/allocation_counter.hh" #include "bench/argv.hh" #include "cli/argument.hh" #include "cli/parser.hh" @@ -12,128 +16,172 @@ namespace { struct Cli20ManyOptions { - cli::StringOption<"opt01"> opt01; - cli::StringOption<"opt02"> opt02; - cli::StringOption<"opt03"> opt03; - cli::StringOption<"opt04"> opt04; - cli::StringOption<"opt05"> opt05; - cli::StringOption<"opt06"> opt06; - cli::StringOption<"opt07"> opt07; - cli::StringOption<"opt08"> opt08; - cli::StringOption<"opt09"> opt09; - cli::StringOption<"opt10"> opt10; - cli::StringOption<"opt11"> opt11; - cli::StringOption<"opt12"> opt12; - cli::StringOption<"opt13"> opt13; - cli::StringOption<"opt14"> opt14; - cli::StringOption<"opt15"> opt15; - cli::StringOption<"opt16"> opt16; + cli::StringOption<"name"> name; + cli::StringOption<"output"> output; + cli::StringOption<"target"> target; + cli::StringOption<"define"> define; + cli::IntOption<"jobs"> jobs; + cli::IntOption<"retries"> retries; + cli::IntOption<"port"> port; + cli::IntOption<"depth"> depth; + cli::DoubleOption<"ratio"> ratio; + cli::DoubleOption<"timeout"> timeout; + cli::DoubleOption<"threshold"> threshold; + cli::DoubleOption<"scale"> scale; + cli::PathOption<"config"> config; + cli::PathOption<"cache"> cache; + cli::Arg<"mode", cli::conversion::choice | + cli::pack::set_once> + mode; + cli::Arg<"color", cli::conversion::choice | + cli::pack::set_once> + color; }; auto many_argv() -> bench::Argv& { - static bench::Argv args{"prog", "--opt01", "v01", "--opt02", "v02", "--opt03", - "v03", "--opt04", "v04", "--opt05", "v05", "--opt06", - "v06", "--opt07", "v07", "--opt08", "v08", "--opt09", - "v09", "--opt10", "v10", "--opt11", "v11", "--opt12", - "v12", "--opt13", "v13", "--opt14", "v14", "--opt15", - "v15", "--opt16", "v16"}; + static bench::Argv args{ + "prog", "--name", "cli20", "--output", "out.o", + "--target", "native", "--define", "NDEBUG", "--jobs", + "12", "--retries", "3", "--port", "8080", + "--depth", "7", "--ratio", "0.75", "--timeout", + "2.5", "--threshold", "0.001", "--scale", "1.25", + "--config", "config.toml", "--cache", "build/cache", "--mode", + "balanced", "--color", "fast"}; return args; } -void add_cli11_options(CLI::App& app, std::string (&values)[16]) { - app.add_option("--opt01", values[0]); - app.add_option("--opt02", values[1]); - app.add_option("--opt03", values[2]); - app.add_option("--opt04", values[3]); - app.add_option("--opt05", values[4]); - app.add_option("--opt06", values[5]); - app.add_option("--opt07", values[6]); - app.add_option("--opt08", values[7]); - app.add_option("--opt09", values[8]); - app.add_option("--opt10", values[9]); - app.add_option("--opt11", values[10]); - app.add_option("--opt12", values[11]); - app.add_option("--opt13", values[12]); - app.add_option("--opt14", values[13]); - app.add_option("--opt15", values[14]); - app.add_option("--opt16", values[15]); +void add_cli11_options(CLI::App& app, bench::ManyValues& values) { + static const std::map modes{ + {"fast", bench::Mode::fast}, + {"balanced", bench::Mode::balanced}, + {"precise", bench::Mode::precise}, + }; + app.add_option("--name", values.name); + app.add_option("--output", values.output); + app.add_option("--target", values.target); + app.add_option("--define", values.define); + app.add_option("--jobs", values.jobs); + app.add_option("--retries", values.retries); + app.add_option("--port", values.port); + app.add_option("--depth", values.depth); + app.add_option("--ratio", values.ratio); + app.add_option("--timeout", values.timeout); + app.add_option("--threshold", values.threshold); + app.add_option("--scale", values.scale); + app.add_option("--config", values.config); + app.add_option("--cache", values.cache); + app.add_option("--mode", values.mode) + ->transform(CLI::CheckedTransformer(modes)); + app.add_option("--color", values.color) + ->transform(CLI::CheckedTransformer(modes)); } void add_argparse_options(argparse::ArgumentParser& program) { - program.add_argument("--opt01"); - program.add_argument("--opt02"); - program.add_argument("--opt03"); - program.add_argument("--opt04"); - program.add_argument("--opt05"); - program.add_argument("--opt06"); - program.add_argument("--opt07"); - program.add_argument("--opt08"); - program.add_argument("--opt09"); - program.add_argument("--opt10"); - program.add_argument("--opt11"); - program.add_argument("--opt12"); - program.add_argument("--opt13"); - program.add_argument("--opt14"); - program.add_argument("--opt15"); - program.add_argument("--opt16"); + program.add_argument("--name"); + program.add_argument("--output"); + program.add_argument("--target"); + program.add_argument("--define"); + program.add_argument("--jobs").scan<'i', int>(); + program.add_argument("--retries").scan<'i', int>(); + program.add_argument("--port").scan<'i', int>(); + program.add_argument("--depth").scan<'i', int>(); + program.add_argument("--ratio").scan<'g', double>(); + program.add_argument("--timeout").scan<'g', double>(); + program.add_argument("--threshold").scan<'g', double>(); + program.add_argument("--scale").scan<'g', double>(); + program.add_argument("--config").action([](const std::string& value) { + return std::filesystem::path{value}; + }); + program.add_argument("--cache").action( + [](const std::string& value) { return std::filesystem::path{value}; }); + program.add_argument("--mode").action(bench::parse_mode_or_throw); + program.add_argument("--color").action(bench::parse_mode_or_throw); } void add_cxxopts_options(cxxopts::Options& options) { - options.add_options()("opt01", "opt01", cxxopts::value())( - "opt02", "opt02", cxxopts::value())( - "opt03", "opt03", cxxopts::value())( - "opt04", "opt04", cxxopts::value())( - "opt05", "opt05", cxxopts::value())( - "opt06", "opt06", cxxopts::value())( - "opt07", "opt07", cxxopts::value())( - "opt08", "opt08", cxxopts::value())( - "opt09", "opt09", cxxopts::value())( - "opt10", "opt10", cxxopts::value())( - "opt11", "opt11", cxxopts::value())( - "opt12", "opt12", cxxopts::value())( - "opt13", "opt13", cxxopts::value())( - "opt14", "opt14", cxxopts::value())( - "opt15", "opt15", cxxopts::value())( - "opt16", "opt16", cxxopts::value()); + options.add_options()("name", "name", cxxopts::value())( + "output", "output", cxxopts::value())( + "target", "target", cxxopts::value())( + "define", "define", cxxopts::value())("jobs", "jobs", + cxxopts::value())( + "retries", "retries", cxxopts::value())("port", "port", + cxxopts::value())( + "depth", "depth", cxxopts::value())("ratio", "ratio", + cxxopts::value())( + "timeout", "timeout", cxxopts::value())("threshold", "threshold", + cxxopts::value())( + "scale", "scale", cxxopts::value())( + "config", "config", cxxopts::value())( + "cache", "cache", cxxopts::value())( + "mode", "mode", cxxopts::value())( + "color", "color", cxxopts::value()); } void BM_Cli20ManyOptions(benchmark::State& state) { auto& args = many_argv(); + std::size_t allocations = 0; + std::size_t bytes = 0; for (auto _ : state) { - auto result = cli::Parser{}.parse(args.span(), 1); - benchmark::DoNotOptimize(result); + const auto stats = bench::measure_allocations([&] { + auto result = cli::Parser{}.parse(args.span(), 1); + benchmark::DoNotOptimize(result); + }); + allocations += stats.allocations; + bytes += stats.bytes; } + bench::set_allocation_counters(state, allocations, bytes); } void BM_Cli11ManyOptions(benchmark::State& state) { auto& args = many_argv(); + std::size_t allocations = 0; + std::size_t bytes = 0; for (auto _ : state) { - CLI::App app{"many_options"}; - std::string values[16]; - add_cli11_options(app, values); - app.parse(args.argc(), args.argv()); - benchmark::DoNotOptimize(values); + const auto stats = bench::measure_allocations([&] { + CLI::App app{"many_options"}; + bench::ManyValues values; + add_cli11_options(app, values); + app.parse(args.argc(), args.argv()); + benchmark::DoNotOptimize(values); + }); + allocations += stats.allocations; + bytes += stats.bytes; } + bench::set_allocation_counters(state, allocations, bytes); } void BM_ArgparseManyOptions(benchmark::State& state) { auto& args = many_argv(); + std::size_t allocations = 0; + std::size_t bytes = 0; for (auto _ : state) { - argparse::ArgumentParser program{"many_options"}; - add_argparse_options(program); - program.parse_args(args.argc(), args.argv()); - benchmark::DoNotOptimize(program); + const auto stats = bench::measure_allocations([&] { + argparse::ArgumentParser program{"many_options"}; + add_argparse_options(program); + program.parse_args(args.argc(), args.argv()); + benchmark::DoNotOptimize(program); + }); + allocations += stats.allocations; + bytes += stats.bytes; } + bench::set_allocation_counters(state, allocations, bytes); } void BM_CxxoptsManyOptions(benchmark::State& state) { auto& args = many_argv(); + std::size_t allocations = 0; + std::size_t bytes = 0; for (auto _ : state) { - cxxopts::Options options{"many_options"}; - add_cxxopts_options(options); - auto result = options.parse(args.argc(), args.argv()); - benchmark::DoNotOptimize(result); + const auto stats = bench::measure_allocations([&] { + cxxopts::Options options{"many_options"}; + add_cxxopts_options(options); + auto result = options.parse(args.argc(), args.argv()); + benchmark::DoNotOptimize(result); + }); + allocations += stats.allocations; + bytes += stats.bytes; } + bench::set_allocation_counters(state, allocations, bytes); } BENCHMARK(BM_Cli20ManyOptions); diff --git a/bench/runtime/simple.cc b/bench/runtime/simple.cc index bde3bb3..f0ac1c9 100644 --- a/bench/runtime/simple.cc +++ b/bench/runtime/simple.cc @@ -5,6 +5,7 @@ #include #include +#include "bench/allocation_counter.hh" #include "bench/argv.hh" #include "cli/argument.hh" #include "cli/parser.hh" @@ -24,53 +25,81 @@ auto simple_argv() -> bench::Argv& { void BM_Cli20Simple(benchmark::State& state) { auto& args = simple_argv(); + std::size_t allocations = 0; + std::size_t bytes = 0; for (auto _ : state) { - auto result = cli::Parser{}.parse(args.span(), 1); - benchmark::DoNotOptimize(result); + const auto stats = bench::measure_allocations([&] { + auto result = cli::Parser{}.parse(args.span(), 1); + benchmark::DoNotOptimize(result); + }); + allocations += stats.allocations; + bytes += stats.bytes; } + bench::set_allocation_counters(state, allocations, bytes); } void BM_Cli11Simple(benchmark::State& state) { auto& args = simple_argv(); + std::size_t allocations = 0; + std::size_t bytes = 0; for (auto _ : state) { - CLI::App app{"simple"}; - bool verbose = false; - std::string output; - std::string input; - app.add_flag("-v,--verbose", verbose); - app.add_option("-o,--output", output); - app.add_option("input", input)->required(); - app.parse(args.argc(), args.argv()); - benchmark::DoNotOptimize(verbose); - benchmark::DoNotOptimize(output); - benchmark::DoNotOptimize(input); + const auto stats = bench::measure_allocations([&] { + CLI::App app{"simple"}; + bool verbose = false; + std::string output; + std::string input; + app.add_flag("-v,--verbose", verbose); + app.add_option("-o,--output", output); + app.add_option("input", input)->required(); + app.parse(args.argc(), args.argv()); + benchmark::DoNotOptimize(verbose); + benchmark::DoNotOptimize(output); + benchmark::DoNotOptimize(input); + }); + allocations += stats.allocations; + bytes += stats.bytes; } + bench::set_allocation_counters(state, allocations, bytes); } void BM_ArgparseSimple(benchmark::State& state) { auto& args = simple_argv(); + std::size_t allocations = 0; + std::size_t bytes = 0; for (auto _ : state) { - argparse::ArgumentParser program{"simple"}; - program.add_argument("-v", "--verbose").flag(); - program.add_argument("-o", "--output"); - program.add_argument("input"); - program.parse_args(args.argc(), args.argv()); - benchmark::DoNotOptimize(program); + const auto stats = bench::measure_allocations([&] { + argparse::ArgumentParser program{"simple"}; + program.add_argument("-v", "--verbose").flag(); + program.add_argument("-o", "--output"); + program.add_argument("input"); + program.parse_args(args.argc(), args.argv()); + benchmark::DoNotOptimize(program); + }); + allocations += stats.allocations; + bytes += stats.bytes; } + bench::set_allocation_counters(state, allocations, bytes); } void BM_CxxoptsSimple(benchmark::State& state) { auto& args = simple_argv(); + std::size_t allocations = 0; + std::size_t bytes = 0; for (auto _ : state) { - cxxopts::Options options{"simple"}; - options.add_options()("v,verbose", "verbose", - cxxopts::value()->default_value("false"))( - "o,output", "output", cxxopts::value())( - "input", "input", cxxopts::value()); - options.parse_positional({"input"}); - auto result = options.parse(args.argc(), args.argv()); - benchmark::DoNotOptimize(result); + const auto stats = bench::measure_allocations([&] { + cxxopts::Options options{"simple"}; + options.add_options()("v,verbose", "verbose", + cxxopts::value()->default_value("false"))( + "o,output", "output", cxxopts::value())( + "input", "input", cxxopts::value()); + options.parse_positional({"input"}); + auto result = options.parse(args.argc(), args.argv()); + benchmark::DoNotOptimize(result); + }); + allocations += stats.allocations; + bytes += stats.bytes; } + bench::set_allocation_counters(state, allocations, bytes); } BENCHMARK(BM_Cli20Simple); diff --git a/bench/runtime/subcommand.cc b/bench/runtime/subcommand.cc index 2b12747..7ac21c2 100644 --- a/bench/runtime/subcommand.cc +++ b/bench/runtime/subcommand.cc @@ -5,6 +5,7 @@ #include #include +#include "bench/allocation_counter.hh" #include "bench/argv.hh" #include "cli/argument.hh" #include "cli/parser.hh" @@ -31,54 +32,83 @@ auto subcommand_argv() -> bench::Argv& { void BM_Cli20Subcommand(benchmark::State& state) { auto& args = subcommand_argv(); + std::size_t allocations = 0; + std::size_t bytes = 0; for (auto _ : state) { - auto result = cli::Parser{}.parse(args.span(), 1); - benchmark::DoNotOptimize(result); + const auto stats = bench::measure_allocations([&] { + auto result = cli::Parser{}.parse(args.span(), 1); + benchmark::DoNotOptimize(result); + }); + allocations += stats.allocations; + bytes += stats.bytes; } + bench::set_allocation_counters(state, allocations, bytes); } void BM_Cli11Subcommand(benchmark::State& state) { auto& args = subcommand_argv(); + std::size_t allocations = 0; + std::size_t bytes = 0; for (auto _ : state) { - CLI::App app{"subcommand"}; - std::string profile; - bool verbose = false; - auto* build = app.add_subcommand("build"); - build->add_option("--profile", profile); - auto* test = app.add_subcommand("test"); - test->add_flag("-v,--verbose", verbose); - app.parse(args.argc(), args.argv()); - benchmark::DoNotOptimize(profile); - benchmark::DoNotOptimize(verbose); + const auto stats = bench::measure_allocations([&] { + CLI::App app{"subcommand"}; + std::string profile; + bool verbose = false; + auto* build = app.add_subcommand("build"); + build->add_option("--profile", profile); + auto* test = app.add_subcommand("test"); + test->add_flag("-v,--verbose", verbose); + app.parse(args.argc(), args.argv()); + benchmark::DoNotOptimize(profile); + benchmark::DoNotOptimize(verbose); + }); + allocations += stats.allocations; + bytes += stats.bytes; } + bench::set_allocation_counters(state, allocations, bytes); } void BM_ArgparseSubcommand(benchmark::State& state) { auto& args = subcommand_argv(); + std::size_t allocations = 0; + std::size_t bytes = 0; for (auto _ : state) { - argparse::ArgumentParser program{"subcommand"}; - argparse::ArgumentParser build{"build"}; - build.add_argument("--profile"); - argparse::ArgumentParser test{"test"}; - test.add_argument("-v", "--verbose").flag(); - program.add_subparser(build); - program.add_subparser(test); - program.parse_args(args.argc(), args.argv()); - benchmark::DoNotOptimize(program); + const auto stats = bench::measure_allocations([&] { + argparse::ArgumentParser program{"subcommand"}; + argparse::ArgumentParser build{"build"}; + build.add_argument("--profile"); + argparse::ArgumentParser test{"test"}; + test.add_argument("-v", "--verbose").flag(); + program.add_subparser(build); + program.add_subparser(test); + program.parse_args(args.argc(), args.argv()); + benchmark::DoNotOptimize(program); + }); + allocations += stats.allocations; + bytes += stats.bytes; } + bench::set_allocation_counters(state, allocations, bytes); } void BM_CxxoptsSubcommand(benchmark::State& state) { auto& args = subcommand_argv(); + std::size_t allocations = 0; + std::size_t bytes = 0; for (auto _ : state) { - const std::string command = args.storage[1]; - if (command == "build") { - cxxopts::Options options{"build"}; - options.add_options()("profile", "profile", cxxopts::value()); - auto result = options.parse(args.argc() - 1, args.argv() + 1); - benchmark::DoNotOptimize(result); - } + const auto stats = bench::measure_allocations([&] { + const std::string command = args.storage[1]; + if (command == "build") { + cxxopts::Options options{"build"}; + options.add_options()("profile", "profile", + cxxopts::value()); + auto result = options.parse(args.argc() - 1, args.argv() + 1); + benchmark::DoNotOptimize(result); + } + }); + allocations += stats.allocations; + bytes += stats.bytes; } + bench::set_allocation_counters(state, allocations, bytes); } BENCHMARK(BM_Cli20Subcommand); diff --git a/bench/scripts/run-runtime.sh b/bench/scripts/run-runtime.sh index ee72547..3b9ba13 100755 --- a/bench/scripts/run-runtime.sh +++ b/bench/scripts/run-runtime.sh @@ -2,14 +2,17 @@ set -euo pipefail repo_root="$(cd "$(dirname "$0")/../.." && pwd)" -build_dir="${BUILD_DIR:-$repo_root/build-bench}" -results_dir="$repo_root/bench/results" +cxx_standard="${BENCH_CXX_STANDARD:-20}" +build_dir="${BUILD_DIR:-$repo_root/build-bench/cxx$cxx_standard}" +results_dir="${RESULTS_DIR:-$repo_root/bench/results}" -cmake -S "$repo_root/bench" -B "$build_dir" -G Ninja -DCMAKE_BUILD_TYPE=Release +cmake -S "$repo_root/bench" -B "$build_dir" -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DBENCH_CXX_STANDARD="$cxx_standard" cmake --build "$build_dir" --target bench_runtime mkdir -p "$results_dir" "$build_dir/runtime/bench_runtime" \ - --benchmark_out="$results_dir/runtime.json" \ + --benchmark_out="$results_dir/runtime_cxx$cxx_standard.json" \ --benchmark_out_format=json \ "$@" diff --git a/bench/scripts/run-size.sh b/bench/scripts/run-size.sh index afb7077..dcc4a5a 100755 --- a/bench/scripts/run-size.sh +++ b/bench/scripts/run-size.sh @@ -2,15 +2,21 @@ set -euo pipefail repo_root="$(cd "$(dirname "$0")/../.." && pwd)" -build_dir="${BUILD_DIR:-$repo_root/build-bench}" -results_dir="$repo_root/bench/results" +cxx_standard="${BENCH_CXX_STANDARD:-20}" +build_dir="${BUILD_DIR:-$repo_root/build-bench/cxx$cxx_standard}" +results_dir="${RESULTS_DIR:-$repo_root/bench/results}" +strip_cmd="${STRIP:-strip}" -cmake -S "$repo_root/bench" -B "$build_dir" -G Ninja -DCMAKE_BUILD_TYPE=Release +cmake -S "$repo_root/bench" -B "$build_dir" -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DBENCH_CXX_STANDARD="$cxx_standard" cmake --build "$build_dir" --target all mkdir -p "$results_dir" -result_file="$results_dir/size.tsv" -printf "case\tlibrary\tbytes\n" > "$result_file" +result_file="$results_dir/size_cxx$cxx_standard.tsv" +stripped_dir="$results_dir/stripped/cxx$cxx_standard" +mkdir -p "$stripped_dir" +printf "standard\tcase\tlibrary\tunstripped_bytes\tstripped_bytes\n" > "$result_file" for binary in "$build_dir"/size/bench_size_*; do [ -x "$binary" ] || continue @@ -19,5 +25,14 @@ for binary in "$build_dir"/size/bench_size_*; do library="${rest##*_}" case_name="${rest%_$library}" bytes="$(wc -c < "$binary" | tr -d ' ')" - printf "%s\t%s\t%s\n" "$case_name" "$library" "$bytes" | tee -a "$result_file" + stripped="$stripped_dir/$name" + cp "$binary" "$stripped" + if "$strip_cmd" "$stripped" >/dev/null 2>&1; then + stripped_bytes="$(wc -c < "$stripped" | tr -d ' ')" + else + stripped_bytes="strip_failed" + fi + printf "c++%s\t%s\t%s\t%s\t%s\n" \ + "$cxx_standard" "$case_name" "$library" "$bytes" "$stripped_bytes" | + tee -a "$result_file" done diff --git a/bench/scripts/summarize.py b/bench/scripts/summarize.py index ca77c4d..77a1034 100755 --- a/bench/scripts/summarize.py +++ b/bench/scripts/summarize.py @@ -3,49 +3,89 @@ import csv import json +import sys +from argparse import ArgumentParser from pathlib import Path ROOT = Path(__file__).resolve().parents[2] -RESULTS = ROOT / "bench" / "results" +RESULTS = Path(__import__("os").environ.get("RESULTS_DIR", ROOT / "bench" / "results")) -def print_tsv(path: Path, title: str) -> None: - if not path.exists(): - return - print(f"\n## {title}") - with path.open(newline="") as f: - rows = list(csv.reader(f, delimiter="\t")) +def read_tsv(paths: list[Path]) -> list[list[str]]: + rows: list[list[str]] = [] + header: list[str] | None = None + for path in paths: + if not path.exists(): + continue + with path.open(newline="") as f: + file_rows = list(csv.reader(f, delimiter="\t")) + if not file_rows: + continue + if header is None: + header = file_rows[0] + rows.append(header) + rows.extend(file_rows[1:]) + return rows + + +def markdown_table(rows: list[tuple[str, ...]] | list[list[str]], title: str) -> str: if not rows: - return - widths = [max(len(row[i]) for row in rows) for i in range(len(rows[0]))] - for row in rows: - print(" ".join(cell.ljust(widths[i]) for i, cell in enumerate(row))) - - -def print_runtime(path: Path) -> None: - if not path.exists(): - return - print("\n## Runtime") - data = json.loads(path.read_text()) - rows = [("name", "time_ns", "cpu_ns")] - for bench in data.get("benchmarks", []): - rows.append( - ( - bench.get("name", ""), - f"{bench.get('real_time', 0):.2f}", - f"{bench.get('cpu_time', 0):.2f}", + return "" + out = [f"## {title}", ""] + out.append("| " + " | ".join(rows[0]) + " |") + out.append("| " + " | ".join("---" for _ in rows[0]) + " |") + for row in rows[1:]: + out.append("| " + " | ".join(row) + " |") + out.append("") + return "\n".join(out) + + +def tsv_table(paths: list[Path], title: str) -> str: + return markdown_table(read_tsv(paths), title) + + +def runtime_table(paths: list[Path]) -> str: + rows = [("standard", "name", "time_ns", "cpu_ns", "allocs", "bytes")] + for path in paths: + if not path.exists(): + continue + standard = path.stem.removeprefix("runtime_") + data = json.loads(path.read_text()) + for bench in data.get("benchmarks", []): + rows.append( + ( + standard, + bench.get("name", ""), + f"{bench.get('real_time', 0):.2f}", + f"{bench.get('cpu_time', 0):.2f}", + f"{bench.get('allocs', 0):.2f}", + f"{bench.get('bytes', 0):.2f}", + ) ) - ) - widths = [max(len(row[i]) for row in rows) for i in range(len(rows[0]))] - for row in rows: - print(" ".join(cell.ljust(widths[i]) for i, cell in enumerate(row))) + return markdown_table(rows, "Runtime") def main() -> None: - print_runtime(RESULTS / "runtime.json") - print_tsv(RESULTS / "size.tsv", "Binary Size") - print_tsv(RESULTS / "compile_time" / "results.tsv", "Compile Time") + parser = ArgumentParser() + parser.add_argument("-o", "--output", type=Path) + args = parser.parse_args() + + sections = [ + "# cli20 benchmark report", + "", + runtime_table(sorted(RESULTS.glob("runtime_cxx*.json"))), + tsv_table(sorted(RESULTS.glob("size_cxx*.tsv")), "Binary Size"), + tsv_table( + sorted(RESULTS.glob("compile_time/cxx*/results.tsv")), + "Compile Time", + ), + ] + report = "\n".join(section for section in sections if section) + if args.output: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(report + "\n") + sys.stdout.write(report + "\n") if __name__ == "__main__": diff --git a/bench/size/many_options/argparse.cc b/bench/size/many_options/argparse.cc index 33b4977..6c7606a 100644 --- a/bench/size/many_options/argparse.cc +++ b/bench/size/many_options/argparse.cc @@ -1,23 +1,28 @@ #include +#include "bench/many_options.hh" + auto main(int argc, char** argv) -> int { argparse::ArgumentParser program{"many_options"}; - program.add_argument("--opt01"); - program.add_argument("--opt02"); - program.add_argument("--opt03"); - program.add_argument("--opt04"); - program.add_argument("--opt05"); - program.add_argument("--opt06"); - program.add_argument("--opt07"); - program.add_argument("--opt08"); - program.add_argument("--opt09"); - program.add_argument("--opt10"); - program.add_argument("--opt11"); - program.add_argument("--opt12"); - program.add_argument("--opt13"); - program.add_argument("--opt14"); - program.add_argument("--opt15"); - program.add_argument("--opt16"); + program.add_argument("--name"); + program.add_argument("--output"); + program.add_argument("--target"); + program.add_argument("--define"); + program.add_argument("--jobs").scan<'i', int>(); + program.add_argument("--retries").scan<'i', int>(); + program.add_argument("--port").scan<'i', int>(); + program.add_argument("--depth").scan<'i', int>(); + program.add_argument("--ratio").scan<'g', double>(); + program.add_argument("--timeout").scan<'g', double>(); + program.add_argument("--threshold").scan<'g', double>(); + program.add_argument("--scale").scan<'g', double>(); + program.add_argument("--config").action([](const std::string& value) { + return std::filesystem::path{value}; + }); + program.add_argument("--cache").action( + [](const std::string& value) { return std::filesystem::path{value}; }); + program.add_argument("--mode").action(bench::parse_mode_or_throw); + program.add_argument("--color").action(bench::parse_mode_or_throw); try { program.parse_args(argc, argv); } catch (const std::runtime_error&) { diff --git a/bench/size/many_options/cli11.cc b/bench/size/many_options/cli11.cc index bb3f547..b8c2e31 100644 --- a/bench/size/many_options/cli11.cc +++ b/bench/size/many_options/cli11.cc @@ -1,25 +1,34 @@ #include -#include +#include + +#include "bench/many_options.hh" auto main(int argc, char** argv) -> int { CLI::App app{"many_options"}; - std::string values[16]; - app.add_option("--opt01", values[0]); - app.add_option("--opt02", values[1]); - app.add_option("--opt03", values[2]); - app.add_option("--opt04", values[3]); - app.add_option("--opt05", values[4]); - app.add_option("--opt06", values[5]); - app.add_option("--opt07", values[6]); - app.add_option("--opt08", values[7]); - app.add_option("--opt09", values[8]); - app.add_option("--opt10", values[9]); - app.add_option("--opt11", values[10]); - app.add_option("--opt12", values[11]); - app.add_option("--opt13", values[12]); - app.add_option("--opt14", values[13]); - app.add_option("--opt15", values[14]); - app.add_option("--opt16", values[15]); + bench::ManyValues values; + static const std::map modes{ + {"fast", bench::Mode::fast}, + {"balanced", bench::Mode::balanced}, + {"precise", bench::Mode::precise}, + }; + app.add_option("--name", values.name); + app.add_option("--output", values.output); + app.add_option("--target", values.target); + app.add_option("--define", values.define); + app.add_option("--jobs", values.jobs); + app.add_option("--retries", values.retries); + app.add_option("--port", values.port); + app.add_option("--depth", values.depth); + app.add_option("--ratio", values.ratio); + app.add_option("--timeout", values.timeout); + app.add_option("--threshold", values.threshold); + app.add_option("--scale", values.scale); + app.add_option("--config", values.config); + app.add_option("--cache", values.cache); + app.add_option("--mode", values.mode) + ->transform(CLI::CheckedTransformer(modes)); + app.add_option("--color", values.color) + ->transform(CLI::CheckedTransformer(modes)); try { app.parse(argc, argv); } catch (const CLI::ParseError& e) { diff --git a/bench/size/many_options/cli20.cc b/bench/size/many_options/cli20.cc index 80c5861..3569f64 100644 --- a/bench/size/many_options/cli20.cc +++ b/bench/size/many_options/cli20.cc @@ -1,23 +1,28 @@ +#include "bench/many_options.hh" #include "cli/argument.hh" #include "cli/parser.hh" struct Args { - cli::StringOption<"opt01"> opt01; - cli::StringOption<"opt02"> opt02; - cli::StringOption<"opt03"> opt03; - cli::StringOption<"opt04"> opt04; - cli::StringOption<"opt05"> opt05; - cli::StringOption<"opt06"> opt06; - cli::StringOption<"opt07"> opt07; - cli::StringOption<"opt08"> opt08; - cli::StringOption<"opt09"> opt09; - cli::StringOption<"opt10"> opt10; - cli::StringOption<"opt11"> opt11; - cli::StringOption<"opt12"> opt12; - cli::StringOption<"opt13"> opt13; - cli::StringOption<"opt14"> opt14; - cli::StringOption<"opt15"> opt15; - cli::StringOption<"opt16"> opt16; + cli::StringOption<"name"> name; + cli::StringOption<"output"> output; + cli::StringOption<"target"> target; + cli::StringOption<"define"> define; + cli::IntOption<"jobs"> jobs; + cli::IntOption<"retries"> retries; + cli::IntOption<"port"> port; + cli::IntOption<"depth"> depth; + cli::DoubleOption<"ratio"> ratio; + cli::DoubleOption<"timeout"> timeout; + cli::DoubleOption<"threshold"> threshold; + cli::DoubleOption<"scale"> scale; + cli::PathOption<"config"> config; + cli::PathOption<"cache"> cache; + cli::Arg<"mode", cli::conversion::choice | + cli::pack::set_once> + mode; + cli::Arg<"color", cli::conversion::choice | + cli::pack::set_once> + color; }; auto main(int argc, char** argv) -> int { diff --git a/bench/size/many_options/cxxopts.cc b/bench/size/many_options/cxxopts.cc index b0a3e65..cdf25fd 100644 --- a/bench/size/many_options/cxxopts.cc +++ b/bench/size/many_options/cxxopts.cc @@ -1,24 +1,25 @@ #include -#include + +#include "bench/many_options.hh" auto main(int argc, char** argv) -> int { cxxopts::Options options{"many_options"}; - options.add_options()("opt01", "opt01", cxxopts::value())( - "opt02", "opt02", cxxopts::value())( - "opt03", "opt03", cxxopts::value())( - "opt04", "opt04", cxxopts::value())( - "opt05", "opt05", cxxopts::value())( - "opt06", "opt06", cxxopts::value())( - "opt07", "opt07", cxxopts::value())( - "opt08", "opt08", cxxopts::value())( - "opt09", "opt09", cxxopts::value())( - "opt10", "opt10", cxxopts::value())( - "opt11", "opt11", cxxopts::value())( - "opt12", "opt12", cxxopts::value())( - "opt13", "opt13", cxxopts::value())( - "opt14", "opt14", cxxopts::value())( - "opt15", "opt15", cxxopts::value())( - "opt16", "opt16", cxxopts::value()); + options.add_options()("name", "name", cxxopts::value())( + "output", "output", cxxopts::value())( + "target", "target", cxxopts::value())( + "define", "define", cxxopts::value())("jobs", "jobs", + cxxopts::value())( + "retries", "retries", cxxopts::value())("port", "port", + cxxopts::value())( + "depth", "depth", cxxopts::value())("ratio", "ratio", + cxxopts::value())( + "timeout", "timeout", cxxopts::value())("threshold", "threshold", + cxxopts::value())( + "scale", "scale", cxxopts::value())( + "config", "config", cxxopts::value())( + "cache", "cache", cxxopts::value())( + "mode", "mode", cxxopts::value())( + "color", "color", cxxopts::value()); try { (void)options.parse(argc, argv); } catch (const cxxopts::exceptions::exception&) { diff --git a/flake.nix b/flake.nix index 10596ea..2a414fe 100644 --- a/flake.nix +++ b/flake.nix @@ -161,6 +161,55 @@ baseUrl = "/cli20/"; }; + packages.bench = pkgs.stdenvNoCC.mkDerivation { + pname = "cli20-bench"; + inherit version; + src = ./.; + + nativeBuildInputs = [ + pkgs.llvmPackages.libcxxClang + pkgs.cmake + pkgs.ninja + pkgs.hyperfine + pkgs.python3 + ]; + + buildInputs = [ + pkgs.cxxopts + pkgs.cli11 + pkgs.argparse + google_bench + ]; + + dontConfigure = true; + + buildPhase = '' + runHook preBuild + + export BUILD_DIR="$TMPDIR/build-bench" + export RESULTS_DIR="$TMPDIR/bench-results" + export HYPERFINE_RUNS=3 + export HYPERFINE_WARMUP=0 + + for standard in 20 26; do + export BENCH_CXX_STANDARD="$standard" + bench/scripts/run-runtime.sh --benchmark_min_time=0.01s + bench/scripts/run-size.sh + bench/compile_time/scripts/run.sh "$RESULTS_DIR/compile_time/cxx$standard" + done + bench/scripts/summarize.py --output "$RESULTS_DIR/report.md" + + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + mkdir -p "$out" + cp -R "$RESULTS_DIR"/. "$out"/ + runHook postInstall + ''; + }; + apps.build = { type = "app"; program = From 528a0f644c6daec10ec089010c80f4d591bb8910 Mon Sep 17 00:00:00 2001 From: gen740 <54583542+gen740@users.noreply.github.com> Date: Tue, 12 May 2026 21:06:13 +0900 Subject: [PATCH 03/15] Remove std::format dependency --- bench/include/bench/many_options.hh | 13 +++--- docs/docs/actions.mdx | 2 +- examples/custom_action.cc | 12 ------ include/cli/action.hh | 33 +++++++++----- include/cli/help.hh | 67 ++++++++++++++++++----------- include/cli/parser.hh | 40 +++++++++-------- 6 files changed, 93 insertions(+), 74 deletions(-) diff --git a/bench/include/bench/many_options.hh b/bench/include/bench/many_options.hh index 7d2fe63..1711442 100644 --- a/bench/include/bench/many_options.hh +++ b/bench/include/bench/many_options.hh @@ -1,9 +1,9 @@ #pragma once #include -#include #include #include +#include #include #include @@ -46,6 +46,10 @@ inline auto operator>>(std::istream& in, Mode& mode) -> std::istream& { return in; } +inline auto operator<<(std::ostream& out, Mode mode) -> std::ostream& { + return out << mode_name(mode); +} + struct ManyValues { std::string name; std::string output; @@ -66,10 +70,3 @@ struct ManyValues { }; } // namespace bench - -template <> -struct std::formatter : std::formatter { - auto format(bench::Mode mode, std::format_context& ctx) const { - return std::formatter::format(bench::mode_name(mode), ctx); - } -}; diff --git a/docs/docs/actions.mdx b/docs/docs/actions.mdx index a89a92e..f46d615 100644 --- a/docs/docs/actions.mdx +++ b/docs/docs/actions.mdx @@ -384,7 +384,7 @@ struct DivisibleBy { cli::detail::validation_failed_error( ctx.index, cli::detail::to_error_subject(input.value), - std::format("must be divisible by {}", Divisor))); + "must be divisible by " + std::to_string(Divisor))); return input; } }; diff --git a/examples/custom_action.cc b/examples/custom_action.cc index f9aaad7..ef50577 100644 --- a/examples/custom_action.cc +++ b/examples/custom_action.cc @@ -1,5 +1,4 @@ #include -#include #include #include #include @@ -12,17 +11,6 @@ struct MinutesSinceMidnight { int value{}; }; -template <> -struct std::formatter { - constexpr auto parse(std::format_parse_context& ctx) { return ctx.begin(); } - - auto format(MinutesSinceMidnight time, std::format_context& ctx) const { - const int hours = time.value / 60; - const int minutes = time.value % 60; - return std::format_to(ctx.out(), "{:02}:{:02}", hours, minutes); - } -}; - struct HHMMConversion { template static constexpr bool accepts_input = diff --git a/include/cli/action.hh b/include/cli/action.hh index 1af0f46..83aa8c3 100644 --- a/include/cli/action.hh +++ b/include/cli/action.hh @@ -1,19 +1,20 @@ #pragma once #include +#include #include #include #include #include #include #include -#include #include #include #include #include #include #include +#include #include #include #include @@ -292,6 +293,16 @@ struct ActionCtx { namespace detail { +template +[[nodiscard]] +auto to_chars_string(T value) -> std::string { + std::array buffer{}; + const auto [ptr, ec] = + std::to_chars(buffer.data(), buffer.data() + buffer.size(), value); + if (ec != std::errc{}) return ""; + return std::string(buffer.data(), ptr); +} + template [[nodiscard]] auto to_error_subject(const T& value) -> std::string { @@ -305,13 +316,14 @@ auto to_error_subject(const T& value) -> std::string { return value == nullptr ? std::string{} : std::string(value); } else if constexpr (std::is_same_v) { return value ? "true" : "false"; - } else if constexpr (std::integral) { - return std::to_string(value); + } else if constexpr (std::integral || std::floating_point) { + return to_chars_string(value); } else if constexpr (std::same_as) { return value.string(); - } else if constexpr (requires { std::format("{}", value); } or - std::floating_point) { - return std::format("{}", value); + } else if constexpr (requires(std::ostream& os, const U& v) { os << v; }) { + std::ostringstream out; + out << value; + return out.str(); } else { return ""; } @@ -1051,7 +1063,7 @@ struct Min { if (input.value < MinValue) { return ActionResult::fail(detail::validation_failed_error( ctx.index, detail::to_error_subject(input.value), - std::format("value must be >= {}", MinValue))); + "value must be >= " + detail::to_error_subject(MinValue))); } return input; } @@ -1081,7 +1093,7 @@ struct Max { if (input.value > MaxValue) { return ActionResult::fail(detail::validation_failed_error( ctx.index, detail::to_error_subject(input.value), - std::format("value must be <= {}", MaxValue))); + "value must be <= " + detail::to_error_subject(MaxValue))); } return input; } @@ -1114,7 +1126,8 @@ struct Range { if (input.value < MinValue || input.value > MaxValue) { return ActionResult::fail(detail::validation_failed_error( ctx.index, detail::to_error_subject(input.value), - std::format("value must be between {} and {}", MinValue, MaxValue))); + "value must be between " + detail::to_error_subject(MinValue) + + " and " + detail::to_error_subject(MaxValue))); } return input; } @@ -1284,7 +1297,7 @@ struct Matches { if (!std::regex_match(input.value.begin(), input.value.end(), regex)) { return ActionResult::fail(detail::validation_failed_error( ctx.index, detail::to_error_subject(input.value), - std::format("value must match {}", Pattern.view()))); + "value must match " + std::string(Pattern.view()))); } return input; } diff --git a/include/cli/help.hh b/include/cli/help.hh index a4b9c40..064955a 100644 --- a/include/cli/help.hh +++ b/include/cli/help.hh @@ -1,7 +1,6 @@ #pragma once #include -#include #include #include #include @@ -124,7 +123,9 @@ template auto metavar_for() -> std::string { using Storage = std::remove_cvref_t; using Base = unwrap_storage_t; - std::string out = std::format("<{}>", MetavarName::value); + std::string out = "<"; + out += MetavarName::value; + out += ">"; if constexpr (UnwrapStorage::variadic) { out.insert(out.size() - 1, "..."); } @@ -197,9 +198,12 @@ auto field_usage_label() -> std::string { if constexpr (std::derived_from) { std::string out; if constexpr (F::short_name != '\0') { - out += std::format("-{}, ", F::short_name); + out += "-"; + out += F::short_name; + out += ", "; } - out += std::format("--{}", F::name.view()); + out += "--"; + out += F::name.view(); if constexpr (!(F::nargs.min == 0 && F::nargs.max == 0)) { out += " "; out += metavar_for(); @@ -223,12 +227,14 @@ auto styled_field_usage_label(const AnsiStyle& style) -> std::string { std::string out; if constexpr (F::short_name != '\0') { out += style.option(); - out += std::format("-{}", F::short_name); + out += "-"; + out += F::short_name; out += reset; out += ", "; } out += style.option(); - out += std::format("--{}", F::name.view()); + out += "--"; + out += F::name.view(); out += reset; if constexpr (!(F::nargs.min == 0 && F::nargs.max == 0)) { out += " "; @@ -238,10 +244,15 @@ auto styled_field_usage_label(const AnsiStyle& style) -> std::string { } return out; } else if constexpr (std::derived_from) { - return std::format("{}{}{}", style.metavar(), - metavar_for(), reset); + std::string out = std::string(style.metavar()); + out += metavar_for(); + out += reset; + return out; } else if constexpr (std::derived_from) { - return std::format("{}{}{}", style.command(), F::command_name(), reset); + std::string out = std::string(style.command()); + out += F::command_name(); + out += reset; + return out; } else { return std::string{}; } @@ -433,7 +444,7 @@ auto relation_operand_label(std::string_view operand) -> std::string { return std::string(operand); } } - return std::format("--{}", operand); + return "--" + std::string(operand); } template @@ -452,22 +463,22 @@ auto relation_help_metadata() -> std::vector { (void)field_name; } else if constexpr (std::same_as) { if (relation_operand_contains(rels.left, field_name)) { - metadata.emplace_back(std::format( - "conflicts: {}", relation_operand_label(rels.right))); + metadata.emplace_back("conflicts: " + + relation_operand_label(rels.right)); } if (relation_operand_contains(rels.right, field_name)) { - metadata.emplace_back(std::format( - "conflicts: {}", relation_operand_label(rels.left))); + metadata.emplace_back("conflicts: " + + relation_operand_label(rels.left)); } } else if constexpr (std::same_as) { if (relation_operand_contains(rels.source, field_name)) { - metadata.emplace_back(std::format( - "requires: {}", relation_operand_label(rels.target))); + metadata.emplace_back( + "requires: " + relation_operand_label(rels.target)); } if (relation_operand_contains(rels.target, field_name)) { metadata.emplace_back( - std::format("required by: {}", - relation_operand_label(rels.source))); + "required by: " + + relation_operand_label(rels.source)); } } }(), @@ -489,7 +500,7 @@ auto help_metadata(Field& field) -> std::vector { metadata.emplace_back("required"); } if (!field.env().empty()) { - metadata.emplace_back(std::format("env: {}", field.env())); + metadata.emplace_back("env: " + std::string(field.env())); } if (const auto& default_value = field.default_value()) { using Value = std::remove_cvref_t; @@ -498,12 +509,12 @@ auto help_metadata(Field& field) -> std::vector { }) { std::ostringstream out; out << *default_value; - metadata.emplace_back(std::format("default: {}", out.str())); + metadata.emplace_back("default: " + out.str()); } } } if (!field.deprecated().empty()) { - metadata.emplace_back(std::format("deprecated: {}", field.deprecated())); + metadata.emplace_back("deprecated: " + std::string(field.deprecated())); } auto relation_metadata = relation_help_metadata(); metadata.insert(metadata.end(), @@ -557,8 +568,9 @@ auto append_group_requirements(std::string& row, std::string_view group_name) using R = std::remove_cvref_t; if constexpr (std::same_as) { if (rels.source == group_name) { - row += std::format(" requires: {}\n", - relation_operand_label(rels.target)); + row += " requires: "; + row += relation_operand_label(rels.target); + row += "\n"; } } }(), @@ -688,8 +700,13 @@ auto format_help_impl(T& value, std::string_view program_name, full_program += command_path; } - std::string out = std::format("{}Usage:{} {}{}\n", style.usage(), reset, - full_program, command_usage_suffix(value)); + std::string out = std::string(style.usage()); + out += "Usage:"; + out += reset; + out += " "; + out += full_program; + out += command_usage_suffix(value); + out += "\n"; if (const auto description = find_description(value); !description.empty()) { out += '\n'; diff --git a/include/cli/parser.hh b/include/cli/parser.hh index 2bc7d3b..07d272e 100644 --- a/include/cli/parser.hh +++ b/include/cli/parser.hh @@ -2,7 +2,6 @@ // #include #include -#include #include #include #include @@ -24,6 +23,20 @@ namespace cli { +namespace detail { + +inline auto value_count_message(int required, int provided) -> std::string { + return "option requires at least " + std::to_string(required) + + " value(s), but " + std::to_string(provided) + " provided"; +} + +inline auto inline_value_count_message(int required) -> std::string { + return "option requires at least " + std::to_string(required) + + " value(s), but inline separator provides only 1"; +} + +} // namespace detail + #ifdef _WIN32 namespace detail { /** @@ -328,9 +341,7 @@ inline auto tokenize(std::span args, if (count < nargs.min) { return result.fail( ErrorCode::missing_value, i, char_full, - std::format("option requires at least {} value(s), but {} " - "provided", - nargs.min, count)); + detail::value_count_message(nargs.min, count)); } } else { // Consume next token(s) greedily as values @@ -356,9 +367,7 @@ inline auto tokenize(std::span args, if (count < nargs.min) { return result.fail( ErrorCode::missing_value, i, char_full, - std::format("option requires at least {} value(s), but {} " - "provided", - nargs.min, count)); + detail::value_count_message(nargs.min, count)); } } advanced_i = true; @@ -385,11 +394,8 @@ inline auto tokenize(std::span args, "flag does not accept a value"); } if (nargs.min > 1) { - return result.fail( - ErrorCode::missing_value, i, opt_name, - std::format("option requires at least {} value(s), but inline " - "separator provides only 1", - nargs.min)); + return result.fail(ErrorCode::missing_value, i, opt_name, + detail::inline_value_count_message(nargs.min)); } push(TokenType::value, tok.substr(eq_pos + 1), i); ++i; @@ -426,10 +432,8 @@ inline auto tokenize(std::span args, } if (count < nargs.min) { - return result.fail( - ErrorCode::missing_value, i, opt_name, - std::format("option requires at least {} value(s), but {} provided", - nargs.min, count)); + return result.fail(ErrorCode::missing_value, i, opt_name, + detail::value_count_message(nargs.min, count)); } continue; } @@ -951,8 +955,8 @@ struct Parser { found = true; f.mark_provided(); auto sub_parser = Parser{}; - sub_parser.set_program_name( - std::format("{} {}", program_name_, F::command_name())); + sub_parser.set_program_name(program_name_ + " " + + std::string(F::command_name())); auto sub = sub_parser.parse(args, first_index); if (sub.has_error()) { res = ActionResult::fail(std::move(sub.error)); From f7dac7438e41c86299f3c1d5c8449d7969720728 Mon Sep 17 00:00:00 2001 From: gen740 <54583542+gen740@users.noreply.github.com> Date: Tue, 12 May 2026 21:40:35 +0900 Subject: [PATCH 04/15] Split parser output helpers --- bench/runtime/many_options.cc | 75 +++++++++++++++++++++++++++-- bench/size/many_options/argparse.cc | 29 ++++++++--- bench/size/many_options/cli11.cc | 32 +++++++++--- bench/size/many_options/cli20.cc | 21 +++++--- bench/size/many_options/cxxopts.cc | 6 +-- include/cli/cli.hh | 1 - include/cli/parser.hh | 24 +++++---- include/cli/parser_iostream.hh | 32 ++++++++++++ include/cli/parser_stdio.hh | 30 ++++++++++++ 9 files changed, 211 insertions(+), 39 deletions(-) create mode 100644 include/cli/parser_iostream.hh create mode 100644 include/cli/parser_stdio.hh diff --git a/bench/runtime/many_options.cc b/bench/runtime/many_options.cc index c821b3b..6a72002 100644 --- a/bench/runtime/many_options.cc +++ b/bench/runtime/many_options.cc @@ -1,17 +1,84 @@ -#include "bench/many_options.hh" - #include #include #include +#include +#include #include +#include +#include #include +#include +#include #include +#include #include "bench/allocation_counter.hh" #include "bench/argv.hh" -#include "cli/argument.hh" -#include "cli/parser.hh" + +namespace bench { + +enum class Mode { fast, balanced, precise }; + +inline auto mode_name(Mode mode) -> std::string_view { + switch (mode) { + case Mode::fast: + return "fast"; + case Mode::balanced: + return "balanced"; + case Mode::precise: + return "precise"; + } + return "unknown"; +} + +inline auto parse_mode(std::string_view value) -> std::optional { + if (value == "fast") return Mode::fast; + if (value == "balanced") return Mode::balanced; + if (value == "precise") return Mode::precise; + return std::nullopt; +} + +inline auto parse_mode_or_throw(const std::string& value) -> Mode { + if (auto mode = parse_mode(value)) return *mode; + throw std::runtime_error("invalid mode: " + value); +} + +inline auto operator>>(std::istream& in, Mode& mode) -> std::istream& { + std::string value; + in >> value; + if (auto parsed = parse_mode(value)) { + mode = *parsed; + } else { + in.setstate(std::ios::failbit); + } + return in; +} + +inline auto operator<<(std::ostream& out, Mode mode) -> std::ostream& { + return out << mode_name(mode); +} + +struct ManyValues { + std::string name; + std::string output; + std::string target; + std::string define; + int jobs{}; + int retries{}; + int port{}; + int depth{}; + double ratio{}; + double timeout{}; + double threshold{}; + double scale{}; + std::filesystem::path config; + std::filesystem::path cache; + Mode mode{}; + Mode color{}; +}; + +} // namespace bench namespace { diff --git a/bench/size/many_options/argparse.cc b/bench/size/many_options/argparse.cc index 6c7606a..50c956a 100644 --- a/bench/size/many_options/argparse.cc +++ b/bench/size/many_options/argparse.cc @@ -1,6 +1,18 @@ #include -#include "bench/many_options.hh" +enum class Mode { fast, balanced, precise }; + +inline auto parse_mode(std::string_view value) -> std::optional { + if (value == "fast") return Mode::fast; + if (value == "balanced") return Mode::balanced; + if (value == "precise") return Mode::precise; + return std::nullopt; +} + +inline auto parse_mode_or_throw(const std::string& value) -> Mode { + if (auto mode = parse_mode(value)) return *mode; + throw std::runtime_error("invalid mode: " + value); +} auto main(int argc, char** argv) -> int { argparse::ArgumentParser program{"many_options"}; @@ -16,13 +28,16 @@ auto main(int argc, char** argv) -> int { program.add_argument("--timeout").scan<'g', double>(); program.add_argument("--threshold").scan<'g', double>(); program.add_argument("--scale").scan<'g', double>(); - program.add_argument("--config").action([](const std::string& value) { - return std::filesystem::path{value}; - }); + program.add_argument("--config") + .action([](const std::string& value) -> std::filesystem::path { + return std::filesystem::path{value}; + }); program.add_argument("--cache").action( - [](const std::string& value) { return std::filesystem::path{value}; }); - program.add_argument("--mode").action(bench::parse_mode_or_throw); - program.add_argument("--color").action(bench::parse_mode_or_throw); + [](const std::string& value) -> std::filesystem::path { + return std::filesystem::path{value}; + }); + program.add_argument("--mode").action(parse_mode_or_throw); + program.add_argument("--color").action(parse_mode_or_throw); try { program.parse_args(argc, argv); } catch (const std::runtime_error&) { diff --git a/bench/size/many_options/cli11.cc b/bench/size/many_options/cli11.cc index b8c2e31..b74a428 100644 --- a/bench/size/many_options/cli11.cc +++ b/bench/size/many_options/cli11.cc @@ -1,15 +1,33 @@ #include -#include -#include "bench/many_options.hh" +enum class Mode { fast, balanced, precise }; + +struct ManyValues { + std::string name; + std::string output; + std::string target; + std::string define; + int jobs{}; + int retries{}; + int port{}; + int depth{}; + double ratio{}; + double timeout{}; + double threshold{}; + double scale{}; + std::filesystem::path config; + std::filesystem::path cache; + Mode mode{}; + Mode color{}; +}; auto main(int argc, char** argv) -> int { CLI::App app{"many_options"}; - bench::ManyValues values; - static const std::map modes{ - {"fast", bench::Mode::fast}, - {"balanced", bench::Mode::balanced}, - {"precise", bench::Mode::precise}, + ManyValues values; + static const std::map modes{ + {"fast", Mode::fast}, + {"balanced", Mode::balanced}, + {"precise", Mode::precise}, }; app.add_option("--name", values.name); app.add_option("--output", values.output); diff --git a/bench/size/many_options/cli20.cc b/bench/size/many_options/cli20.cc index 3569f64..0067072 100644 --- a/bench/size/many_options/cli20.cc +++ b/bench/size/many_options/cli20.cc @@ -1,6 +1,13 @@ -#include "bench/many_options.hh" -#include "cli/argument.hh" -#include "cli/parser.hh" +#include + +enum class Mode { fast, balanced, precise }; + +inline auto parse_mode(std::string_view value) -> std::optional { + if (value == "fast") return Mode::fast; + if (value == "balanced") return Mode::balanced; + if (value == "precise") return Mode::precise; + return std::nullopt; +} struct Args { cli::StringOption<"name"> name; @@ -17,11 +24,11 @@ struct Args { cli::DoubleOption<"scale"> scale; cli::PathOption<"config"> config; cli::PathOption<"cache"> cache; - cli::Arg<"mode", cli::conversion::choice | - cli::pack::set_once> + cli::Arg<"mode", + cli::conversion::choice | cli::pack::set_once> mode; - cli::Arg<"color", cli::conversion::choice | - cli::pack::set_once> + cli::Arg<"color", + cli::conversion::choice | cli::pack::set_once> color; }; diff --git a/bench/size/many_options/cxxopts.cc b/bench/size/many_options/cxxopts.cc index cdf25fd..33e95d5 100644 --- a/bench/size/many_options/cxxopts.cc +++ b/bench/size/many_options/cxxopts.cc @@ -1,6 +1,6 @@ #include -#include "bench/many_options.hh" +enum class Mode { fast, balanced, precise }; auto main(int argc, char** argv) -> int { cxxopts::Options options{"many_options"}; @@ -18,8 +18,8 @@ auto main(int argc, char** argv) -> int { "scale", "scale", cxxopts::value())( "config", "config", cxxopts::value())( "cache", "cache", cxxopts::value())( - "mode", "mode", cxxopts::value())( - "color", "color", cxxopts::value()); + "mode", "mode", cxxopts::value())("color", "color", + cxxopts::value()); try { (void)options.parse(argc, argv); } catch (const cxxopts::exceptions::exception&) { diff --git a/include/cli/cli.hh b/include/cli/cli.hh index e001b99..3cb2c59 100644 --- a/include/cli/cli.hh +++ b/include/cli/cli.hh @@ -1,4 +1,3 @@ #pragma once -#include "cli/argument.hh" #include "cli/parser.hh" diff --git a/include/cli/parser.hh b/include/cli/parser.hh index 07d272e..d0536ee 100644 --- a/include/cli/parser.hh +++ b/include/cli/parser.hh @@ -1,8 +1,8 @@ #pragma once // #include +#include #include -#include #include #include #include @@ -25,6 +25,12 @@ namespace cli { namespace detail { +inline auto write_message(FILE* file, std::string_view message) -> void { + if (file == nullptr || message.empty()) return; + (void)std::fwrite(message.data(), 1, message.size(), file); + (void)std::fwrite("\n", 1, 1, file); +} + inline auto value_count_message(int required, int provided) -> std::string { return "option requires at least " + std::to_string(required) + " value(s), but " + std::to_string(provided) + " provided"; @@ -1196,9 +1202,10 @@ auto parse(int argc, char* argv[]) -> ParseResult { /** * @brief Parses `argc`/`argv` and exits the process on failure. * - * If parsing fails, writes the error message to `err` (or `out` for - * help/exit-success) and calls `std::exit` with the appropriate exit code. On - * success, returns the fully populated argument struct by value. + * If parsing fails, writes the error message directly to `stderr` (or `stdout` + * for help/exit-success) using stdio and calls `std::exit` with the appropriate + * exit code. Include `cli/parser_stdio.hh` or `cli/parser_iostream.hh` when the + * output destination must be customized. * * This is the typical one-liner entry point for command-line tools: * @code @@ -1208,19 +1215,16 @@ auto parse(int argc, char* argv[]) -> ParseResult { * @tparam T The argument specification type. * @param argc Argument count (includes program name at index 0). * @param argv Argument vector. - * @param out Output stream for help/success messages. Default: `std::cout`. - * @param err Output stream for error messages. Default: `std::cerr`. * @return The parsed argument struct (never returns on failure). */ template // NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays, modernize-avoid-c-arrays) -auto parse_or_exit(int argc, char* argv[], std::ostream& out = std::cout, - std::ostream& err = std::cerr) -> T { +auto parse_or_exit(int argc, char* argv[]) -> T { auto result = parse(argc, argv); if (!result) { if (const auto message = result.error.message(); !message.empty()) { - auto& stream = result.error.use_stdout() ? out : err; - stream << message << '\n'; + detail::write_message(result.error.use_stdout() ? stdout : stderr, + message); } std::exit(result.error.exit_code()); } diff --git a/include/cli/parser_iostream.hh b/include/cli/parser_iostream.hh new file mode 100644 index 0000000..1b6a888 --- /dev/null +++ b/include/cli/parser_iostream.hh @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +#include "cli/parser.hh" + +namespace cli { + +/** + * @brief Parses `argc`/`argv` and exits the process on failure. + * + * This overload writes help/exit-success messages to `out` and errors to `err` + * using iostreams. Include this header only when stream-based output + * customization is needed. + */ +template +// NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays, modernize-avoid-c-arrays) +auto parse_or_exit(int argc, char* argv[], std::ostream& out, std::ostream& err) + -> T { + auto result = parse(argc, argv); + if (!result) { + if (const auto message = result.error.message(); !message.empty()) { + auto& stream = result.error.use_stdout() ? out : err; + stream << message << '\n'; + } + std::exit(result.error.exit_code()); + } + return std::move(result.value); +} + +} // namespace cli diff --git a/include/cli/parser_stdio.hh b/include/cli/parser_stdio.hh new file mode 100644 index 0000000..089b267 --- /dev/null +++ b/include/cli/parser_stdio.hh @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +#include "cli/parser.hh" + +namespace cli { + +/** + * @brief Parses `argc`/`argv` and exits the process on failure. + * + * This overload writes help/exit-success messages to `out` and errors to `err` + * using stdio. Include this header when the FILE destinations must be + * customized; `parser.hh` provides the simpler stdout/stderr fixed version. + */ +template +// NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays, modernize-avoid-c-arrays) +auto parse_or_exit(int argc, char* argv[], FILE* out, FILE* err) -> T { + auto result = parse(argc, argv); + if (!result) { + if (const auto message = result.error.message(); !message.empty()) { + detail::write_message(result.error.use_stdout() ? out : err, message); + } + std::exit(result.error.exit_code()); + } + return std::move(result.value); +} + +} // namespace cli From 04f00cbcfa1a336dd69d241348dc629d47c020cf Mon Sep 17 00:00:00 2001 From: gen740 <54583542+gen740@users.noreply.github.com> Date: Tue, 12 May 2026 21:43:49 +0900 Subject: [PATCH 05/15] Fix cxxopts many-options size benchmark --- bench/size/many_options/cxxopts.cc | 39 ++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/bench/size/many_options/cxxopts.cc b/bench/size/many_options/cxxopts.cc index 33e95d5..f3404e8 100644 --- a/bench/size/many_options/cxxopts.cc +++ b/bench/size/many_options/cxxopts.cc @@ -2,6 +2,45 @@ enum class Mode { fast, balanced, precise }; +inline auto mode_name(Mode mode) -> std::string_view { + switch (mode) { + case Mode::fast: + return "fast"; + case Mode::balanced: + return "balanced"; + case Mode::precise: + return "precise"; + } + return "unknown"; +} + +inline auto parse_mode(std::string_view value) -> std::optional { + if (value == "fast") return Mode::fast; + if (value == "balanced") return Mode::balanced; + if (value == "precise") return Mode::precise; + return std::nullopt; +} + +inline auto parse_mode_or_throw(const std::string& value) -> Mode { + if (auto mode = parse_mode(value)) return *mode; + throw std::runtime_error("invalid mode: " + value); +} + +inline auto operator>>(std::istream& in, Mode& mode) -> std::istream& { + std::string value; + in >> value; + if (auto parsed = parse_mode(value)) { + mode = *parsed; + } else { + in.setstate(std::ios::failbit); + } + return in; +} + +inline auto operator<<(std::ostream& out, Mode mode) -> std::ostream& { + return out << mode_name(mode); +} + auto main(int argc, char** argv) -> int { cxxopts::Options options{"many_options"}; options.add_options()("name", "name", cxxopts::value())( From a7cc13fb10d49e6213a3b33d370dcfcf728eb3e8 Mon Sep 17 00:00:00 2001 From: gen740 <54583542+gen740@users.noreply.github.com> Date: Tue, 12 May 2026 23:29:44 +0900 Subject: [PATCH 06/15] refactor(parser): optimize tokenizer and eliminate map-based lookup Replace heap-allocated containers in the tokenizer with stack-buffered alternatives and eliminate the overhead of building temporary maps for option lookup. Use bit-packed prefix representations and smaller integer types in tokens to reduce memory footprint. - Use SmallVector for tokens to avoid heap usage in typical cases. - Replace map-based lookup with on-the-fly field iteration. - Refactor TokenizerConfig to use constexpr and std::string_view. - Update Description to use std::string_view instead of std::string. - Configure Nix shell with venvShellHook for Python environments. --- flake.nix | 3 +- include/cli/argument.hh | 7 +- include/cli/parser.hh | 380 ++++++++++++++++++++++++++++++---------- tests/test_tokenize.cc | 62 +++++-- 4 files changed, 336 insertions(+), 116 deletions(-) diff --git a/flake.nix b/flake.nix index 2a414fe..612d5e0 100644 --- a/flake.nix +++ b/flake.nix @@ -87,8 +87,9 @@ google_bench config.treefmt.build.wrapper pkgs.hyperfine - pkgs.python3 + pkgs.python3Packages.venvShellHook ]; + venvDir = "venv"; }; packages.default = pkgs.stdenvNoCC.mkDerivation { diff --git a/include/cli/argument.hh b/include/cli/argument.hh index be8dee3..d129ded 100644 --- a/include/cli/argument.hh +++ b/include/cli/argument.hh @@ -375,7 +375,12 @@ struct PositionalImpl : public PositionalTag { * }; * @endcode */ -struct Description : public std::string, DescriptionTag {}; +struct Description : DescriptionTag { + std::string_view text{}; + constexpr Description() = default; + constexpr Description(std::string_view s) : text(s) {} + constexpr operator std::string_view() const { return text; } +}; /** * @brief Parameter bag used when constructing a `Command` field. diff --git a/include/cli/parser.hh b/include/cli/parser.hh index d0536ee..e09a62a 100644 --- a/include/cli/parser.hh +++ b/include/cli/parser.hh @@ -6,8 +6,6 @@ #include #include #include -#include -#include #include #ifdef _WIN32 @@ -119,26 +117,45 @@ inline auto utf16_to_utf8(int argc, wchar_t* argv[]) -> Utf8Argv { * - `command` : A subcommand name; subsequent tokens belong to the * subcommand. */ -enum class TokenType { +enum class TokenType : uint8_t { option, value, positional, command, }; +static constexpr std::uint8_t kNoPrefix = 0xff; +static constexpr std::uint8_t kShortPrefixBit = 0x80; +static constexpr std::uint8_t kPrefixIndexMask = 0x7f; + +static constexpr auto no_prefix() -> std::uint8_t { return kNoPrefix; } + +static constexpr auto short_prefix() -> std::uint8_t { return kShortPrefixBit; } + +static constexpr auto long_prefix(std::uint8_t i) -> std::uint8_t { + return i & kPrefixIndexMask; +} + +static constexpr auto has_prefix(std::uint8_t p) -> bool { + return p != kNoPrefix; +} + +static constexpr auto is_short_prefix(std::uint8_t p) -> bool { + return p != kNoPrefix && (p & kShortPrefixBit); +} + /** * @brief A single token produced by `tokenize()`. */ struct Token { - TokenType type; ///< Classification of this token. - std::string_view text; ///< For option tokens: the bare name without prefix - ///< (e.g. `"verbose"` for `--verbose`, `"v"` for - ///< `-v`). For value/positional/command tokens: the - ///< raw text from the args span. - std::string_view matched_prefix; ///< The option prefix that was matched (e.g. - ///< `"--"` or `"-"`); empty for non-option - ///< tokens. - std::size_t position; ///< Zero-based index into the original `args` span. + std::string_view text; ///< For option tokens: the bare name without prefix + ///< (e.g. `"verbose"` for `--verbose`, `"v"` for + ///< `-v`). For value/positional/command tokens: the + ///< raw text from the args span. + std::uint16_t position; ///< Zero-based index into the original `args` span. + std::uint8_t prefix; // 0xff = none, high bit = short prefix, low 7bit = long + // prefix index + TokenType type; ///< Classification of this token. }; /** @@ -149,7 +166,53 @@ struct Token { * `error` describes the first problem encountered. */ struct TokenizeResult { - std::vector tokens; + template + + struct SmallVector { + std::array stack{}; + std::vector heap; + std::size_t size_value{}; + + auto push_back(T value) -> void { + if (size_value < N && heap.empty()) { + stack[size_value++] = std::move(value); + return; + } + + if (heap.empty()) { + heap.reserve(N * 2); + for (std::size_t i = 0; i < size_value; ++i) { + heap.push_back(std::move(stack[i])); + } + } + + heap.push_back(std::move(value)); + ++size_value; + } + + [[nodiscard]] auto size() const -> std::size_t { return size_value; } + [[nodiscard]] auto empty() const -> bool { return size_value == 0; } + + auto operator[](std::size_t i) -> T& { + return heap.empty() ? stack[i] : heap[i]; + } + + auto operator[](std::size_t i) const -> const T& { + return heap.empty() ? stack[i] : heap[i]; + } + + [[nodiscard]] auto begin() const { + return heap.empty() ? stack.data() : heap.data(); + } + + [[nodiscard]] auto end() const { return begin() + size_value; } + + [[nodiscard]] auto data() const { + return heap.empty() ? stack.data() : heap.data(); + } + }; + + SmallVector tokens; ParseError error{}; /** @brief Returns `true` when tokenization succeeded. */ @@ -202,18 +265,96 @@ struct TokenizeResult { * `=` as the inline value separator and `--` as the end-of-options marker. */ struct TokenizerConfig { - std::vector option_prefixes = { - "--"}; ///< Prefixes that introduce long options. - std::string short_option_prefix = - "-"; ///< Prefix for short (single-character) options. - std::string end_of_options_separator = - "--"; ///< Token that ends option processing; subsequent tokens become - ///< positionals. - char inline_value_separator = - '='; ///< Character separating option name from inline value (e.g. `=` for - ///< `--opt=val`). + private: + static constexpr std::size_t max_option_prefixes = 4; + + std::array option_prefixes_{"--"}; + std::uint8_t option_prefix_count_{1}; + + std::string_view short_option_prefix_{"-"}; + std::string_view end_of_options_separator_{"--"}; + char inline_value_separator_{'='}; + + public: + constexpr TokenizerConfig() = default; + + [[nodiscard]] constexpr auto option_prefixes() const + -> std::span { + return {option_prefixes_.data(), option_prefix_count_}; + } + + [[nodiscard]] constexpr auto short_option_prefix() const -> std::string_view { + return short_option_prefix_; + } + + [[nodiscard]] constexpr auto end_of_options_separator() const + -> std::string_view { + return end_of_options_separator_; + } + + [[nodiscard]] constexpr auto inline_value_separator() const -> char { + return inline_value_separator_; + } + + constexpr auto set_option_prefixes( + std::initializer_list prefixes) -> void { + option_prefix_count_ = 0; + + for (auto p : prefixes) { + if (option_prefix_count_ >= max_option_prefixes) break; + option_prefixes_[option_prefix_count_++] = p; + } + } + + [[nodiscard]] constexpr auto with_option_prefixes( + std::initializer_list prefixes) const + -> TokenizerConfig { + auto cfg = *this; + cfg.set_option_prefixes(prefixes); + return cfg; + } + + [[nodiscard]] constexpr auto with_short_option_prefix( + std::string_view prefix) const -> TokenizerConfig { + auto cfg = *this; + cfg.short_option_prefix_ = prefix; + return cfg; + } + + [[nodiscard]] constexpr auto with_end_of_options_separator( + std::string_view separator) const -> TokenizerConfig { + auto cfg = *this; + cfg.end_of_options_separator_ = separator; + return cfg; + } + + [[nodiscard]] constexpr auto with_inline_value_separator(char separator) const + -> TokenizerConfig { + auto cfg = *this; + cfg.inline_value_separator_ = separator; + return cfg; + } }; +/** + * @brief Retrieves the matched prefix string for an option token. + * + * For a token with `prefix == kNoPrefix`, returns an empty view. For a short + * prefix, returns `cfg.short_option_prefix`. For a long prefix, returns the + * corresponding entry from `cfg.option_prefixes`. + * + * @param tok The token whose prefix to retrieve; must be of type `option`. + * @param cfg The tokenizer configuration that was used to parse `tok`. + * @return The matched prefix string as a view into `cfg`, or empty if no + * prefix. + */ +inline auto token_prefix_view(const Token& tok, const TokenizerConfig& cfg) + -> std::string_view { + if (tok.prefix == kNoPrefix) return {}; + if (tok.prefix & kShortPrefixBit) return cfg.short_option_prefix(); + return cfg.option_prefixes()[tok.prefix & kPrefixIndexMask]; +} + /** * @brief Converts a raw argument span into an ordered sequence of typed tokens. * @@ -240,28 +381,44 @@ struct TokenizerConfig { * @return A `TokenizeResult` containing the token list on success, or a * `ParseError` on failure. */ +template inline auto tokenize(std::span args, - const std::unordered_map& spec_map, - const std::unordered_set& command_names = {}, + OptionLookup option_nargs, CommandLookup is_command, const TokenizerConfig& cfg = {}) -> TokenizeResult { TokenizeResult result; // Returns the matched prefix (as a view into cfg.option_prefixes) or empty. - const auto find_prefix = [&](std::string_view tok) -> std::string_view { - for (const auto& p : cfg.option_prefixes) { - if (tok.size() > p.size() && tok.starts_with(p)) return p; + const auto find_prefix = [&](std::string_view tok) -> std::uint8_t { + for (std::uint8_t pi = 0; + pi < static_cast(cfg.option_prefixes().size()); ++pi) { + const auto& p = cfg.option_prefixes()[pi]; + if (tok.size() > p.size() && tok.starts_with(p)) { + return long_prefix(pi); + } } - if (tok.size() > cfg.short_option_prefix.size() && - tok.starts_with(cfg.short_option_prefix)) { - return cfg.short_option_prefix; + + if (tok.size() > cfg.short_option_prefix().size() && + tok.starts_with(cfg.short_option_prefix())) { + return short_prefix(); } - return {}; + + return no_prefix(); }; - const auto push = [&](TokenType type, std::string_view text, std::size_t pos, - std::string_view prefix = {}) -> void { - result.tokens.push_back( - {.type = type, .text = text, .matched_prefix = prefix, .position = pos}); + const auto prefix_view = [&](std::uint8_t p) -> std::string_view { + if (p == no_prefix()) return {}; + if (is_short_prefix(p)) return cfg.short_option_prefix(); + return cfg.option_prefixes()[p & kPrefixIndexMask]; + }; + + const auto push = [&](TokenType type, std::string_view text, std::uint16_t pos, + std::uint8_t prefix = kNoPrefix) -> void { + result.tokens.push_back({ + .text = text, + .position = pos, + .prefix = prefix, + .type = type, + }); }; bool post_separator = false; @@ -271,7 +428,7 @@ inline auto tokenize(std::span args, const std::string_view tok = args[i]; // ── option parsing terminator ───────────────────────────────────────── - if (!post_separator && tok == cfg.end_of_options_separator) { + if (!post_separator && tok == cfg.end_of_options_separator()) { post_separator = true; ++i; continue; @@ -285,59 +442,59 @@ inline auto tokenize(std::span args, // ── command name ────────────────────────────────────────────────────── const auto prefix = find_prefix(tok); - if (prefix.empty() && command_names.contains(std::string(tok))) { + const auto prefix_sv = prefix_view(prefix); + if (!has_prefix(prefix) && is_command(tok)) { push(TokenType::command, tok, i); break; // args[i+1:] are the sub-command's responsibility } // ── option-looking token ────────────────────────────────────────────── - if (!prefix.empty()) { - const auto sep = cfg.inline_value_separator; + if (has_prefix(prefix)) { + const auto sep = cfg.inline_value_separator(); const auto eq_pos = tok.find(sep); const bool has_inline_val = (eq_pos != std::string_view::npos); const std::string_view opt_sv = has_inline_val ? tok.substr(0, eq_pos) : tok; const std::string opt_name{opt_sv}; - - const auto spec_it = spec_map.find(opt_name); - if (spec_it == spec_map.end()) { + const auto nargs_opt = option_nargs(opt_name); + if (!nargs_opt) { // Short cluster (-xvf) or attached value (-ofile): only when the token // uses the short prefix and carries more than one character after it. - const std::string_view body = opt_sv.substr(prefix.size()); - if (prefix == cfg.short_option_prefix && !has_inline_val && - body.size() > 1) { + const std::string_view body = opt_sv.substr(prefix_sv.size()); + if (is_short_prefix(prefix) && !has_inline_val && body.size() > 1) { bool advanced_i = false; - const std::string_view char_prefix = tok.substr(0, prefix.size()); + const std::string_view char_prefix = tok.substr(0, prefix_sv.size()); std::string char_full{char_prefix}; char_full.push_back('\0'); for (std::size_t k = 0; k < body.size(); ++k) { - const std::string_view char_bare = tok.substr(prefix.size() + k, 1); + const std::string_view char_bare = + tok.substr(prefix_sv.size() + k, 1); char_full.back() = body[k]; - const auto it = spec_map.find(char_full); - if (it == spec_map.end()) { + const auto nargs_opt = option_nargs(char_full); + if (!nargs_opt) { return result.fail(ErrorCode::unknown_option, i, char_full); } - const Nargs& nargs = it->second; + const Nargs nargs = *nargs_opt; const bool is_flag = (nargs.min == 0 && nargs.max == 0); - push(TokenType::option, char_bare, i, char_prefix); + push(TokenType::option, char_bare, i, prefix); if (!is_flag) { if (k + 1 < body.size()) { // First value is attached (rest of body). - push(TokenType::value, tok.substr(prefix.size() + k + 1), i); + push(TokenType::value, tok.substr(prefix_sv.size() + k + 1), i); ++i; // Consume additional tokens for nargs > 1. int count = 1; while (i < args.size()) { const std::string_view next = args[i]; - if (next == cfg.end_of_options_separator) break; - if (command_names.contains(std::string(next))) break; + if (next == cfg.end_of_options_separator()) break; + if (is_command(std::string(next))) break; if (nargs.max != -1 && count >= nargs.max) break; - if (!find_prefix(next).empty()) { + if (has_prefix(find_prefix(next))) { const auto next_sep = next.find(sep); const std::string next_opt{next_sep != std::string_view::npos ? next.substr(0, next_sep) : next}; - if (spec_map.contains(next_opt)) break; + if (option_nargs(next_opt).has_value()) break; if (count >= 1 || nargs.min == 0) break; } push(TokenType::value, args[i], i); @@ -355,15 +512,15 @@ inline auto tokenize(std::span args, int count = 0; while (i < args.size()) { const std::string_view next = args[i]; - if (next == cfg.end_of_options_separator) break; - if (command_names.contains(std::string(next))) break; + if (next == cfg.end_of_options_separator()) break; + if (is_command(std::string(next))) break; if (nargs.max != -1 && count >= nargs.max) break; - if (!find_prefix(next).empty()) { + if (has_prefix(find_prefix(next))) { const auto next_sep = next.find(sep); const std::string next_opt{next_sep != std::string_view::npos ? next.substr(0, next_sep) : next}; - if (spec_map.contains(next_opt)) break; + if (option_nargs(next_opt).has_value()) break; if (count >= 1 || nargs.min == 0) break; } push(TokenType::value, args[i], i); @@ -386,13 +543,13 @@ inline auto tokenize(std::span args, return result.fail(ErrorCode::unknown_option, i, opt_name); } - const Nargs& nargs = spec_it->second; + const Nargs& nargs = *nargs_opt; const bool is_flag = (nargs.min == 0 && nargs.max == 0); // Store bare name (without prefix) in text; matched_prefix holds the // prefix. - push(TokenType::option, opt_sv.substr(prefix.size()), i, - opt_sv.substr(0, prefix.size())); + + push(TokenType::option, opt_sv.substr(prefix_sv.size()), i, prefix); if (has_inline_val) { if (is_flag) { @@ -417,16 +574,16 @@ inline auto tokenize(std::span args, while (i < args.size()) { const std::string_view next = args[i]; - if (next == cfg.end_of_options_separator) break; - if (command_names.contains(std::string(next))) break; + if (next == cfg.end_of_options_separator()) break; + if (is_command(std::string(next))) break; if (nargs.max != -1 && count >= nargs.max) break; - if (!find_prefix(next).empty()) { + if (has_prefix(find_prefix(next))) { const auto next_sep = next.find(sep); const std::string next_opt{next_sep != std::string_view::npos ? next.substr(0, next_sep) : next}; - if (spec_map.contains(next_opt)) break; + if (option_nargs(next_opt).has_value()) break; // Unknown option-looking: consume only as first value when min > 0 if (count >= 1 || nargs.min == 0) break; @@ -690,9 +847,13 @@ struct Parser { auto parse_body(ParseResult& result, std::span args, std::size_t first_index) -> void { - auto [spec_map, command_names] = get_option_spec_map(result.value); - auto tokenized = tokenize(args.subspan(std::min(first_index, args.size())), - spec_map, command_names, cfg_); + auto lookup = get_option_spec_map(result.value); + + auto tokenized = tokenize( + args.subspan(std::min(first_index, args.size())), + [&](std::string_view opt) -> auto { return lookup.option_nargs(opt); }, + [&](std::string_view name) -> auto { return lookup.is_command(name); }, + cfg_); if (tokenized.has_error()) { if (tokenized.error.has_position()) { tokenized.error.position += static_cast(first_index); @@ -701,6 +862,12 @@ struct Parser { return; } + const auto prefix_view = [&](std::uint8_t p) -> std::string_view { + if (p == no_prefix()) return {}; + if (is_short_prefix(p)) return cfg_.short_option_prefix(); + return cfg_.option_prefixes()[p & kPrefixIndexMask]; + }; + const auto& tokens = tokenized.tokens; std::size_t pos_idx = 0, pos_cnt = 0; @@ -710,9 +877,10 @@ struct Parser { if (tok.type == TokenType::option) { std::size_t j = i + 1; while (j < tokens.size() && tokens[j].type == TokenType::value) ++j; - auto err = dispatch_option(result.value, tok.matched_prefix, tok.text, - std::span(tokens).subspan(i + 1, j - i - 1), - first_index, tok.position); + auto err = + dispatch_option(result.value, prefix_view(tok.prefix), tok.text, + std::span(tokens).subspan(i + 1, j - i - 1), + first_index, tok.position); if (err.has_error()) { result.error = finalize_special_error(std::move(err.error)); return; @@ -843,25 +1011,47 @@ struct Parser { } // Build the option spec-map and command-name set from the field types of val. - auto get_option_spec_map(const T& val) - -> std::pair, - std::unordered_set> { - std::unordered_map spec_map; - std::unordered_set command_names; - for_each_field(val, [&](const auto& f) -> auto { - using F = std::remove_cvref_t; - if constexpr (std::derived_from) { - for (const auto& p : cfg_.option_prefixes) - spec_map.emplace(p + std::string(F::name.view()), F::nargs); - if constexpr (F::short_name != '\0') - spec_map.emplace( - cfg_.short_option_prefix + std::string(1, F::short_name), - F::nargs); - } - if constexpr (std::derived_from) - command_names.emplace(std::string(F::command_name())); - }); - return {spec_map, command_names}; + struct Lookup { + const Parser* self; + const T* val; + + [[nodiscard]] auto option_nargs(std::string_view opt) const + -> std::optional { + std::optional found; + self->for_each_field(*val, [&](const auto& f) -> auto { + using F = std::remove_cvref_t; + if constexpr (std::derived_from) { + for (const auto& p : self->cfg_.option_prefixes()) { + if (opt.starts_with(p) && opt.substr(p.size()) == F::name.view()) { + found = F::nargs; + } + } + if constexpr (F::short_name != '\0') { + const auto& sp = self->cfg_.short_option_prefix(); + if (opt.starts_with(sp) && opt.size() == sp.size() + 1 && + opt.back() == F::short_name) { + found = F::nargs; + } + } + } + }); + return found; + } + + [[nodiscard]] auto is_command(std::string_view name) const -> bool { + bool found = false; + self->for_each_field(*val, [&](const auto& f) -> auto { + using F = std::remove_cvref_t; + if constexpr (std::derived_from) { + if (F::command_name() == name) found = true; + } + }); + return found; + } + }; + + auto get_option_spec_map(const T& val) const -> Lookup { + return Lookup{.self = this, .val = &val}; } // Find the option field matching (prefix, bare), notify it, and invoke its @@ -880,14 +1070,14 @@ struct Parser { return; } bool match = false; - for (const auto& p : cfg_.option_prefixes) { + for (const auto& p : cfg_.option_prefixes()) { if (p == prefix && F::name.view() == bare) { match = true; break; } } if (!match && F::short_name != '\0' && - cfg_.short_option_prefix == prefix && bare.size() == 1 && + cfg_.short_option_prefix() == prefix && bare.size() == 1 && bare[0] == F::short_name) { match = true; } diff --git a/tests/test_tokenize.cc b/tests/test_tokenize.cc index 7cf3791..6d930c4 100644 --- a/tests/test_tokenize.cc +++ b/tests/test_tokenize.cc @@ -1,5 +1,8 @@ #include +#include +#include + #include "cli/parser.hh" // ---- Test infrastructure -------------------------------------------------- @@ -25,11 +28,21 @@ static auto tok(std::initializer_list il) { return std::vector(il); } -// Invoke tokenize with vector args. static auto tokenize(std::initializer_list args, const SpecMap& spec, const CmdSet& cmds = {}) -> TokenizeResult { - return cli::tokenize(tok(args), spec, cmds); + auto option_nargs = [&](std::string_view opt) -> std::optional { + if (auto it = spec.find(std::string(opt)); it != spec.end()) { + return it->second; + } + return std::nullopt; + }; + + auto is_command = [&](std::string_view name) -> bool { + return cmds.contains(std::string(name)); + }; + + return cli::tokenize(tok(args), option_nargs, is_command); } // Collect texts of tokens with the given type, in order. @@ -84,7 +97,7 @@ TEST(Tokenize, FlagEmitsOptionTokenOnly) { ASSERT_EQ(r.tokens.size(), 1u); EXPECT_EQ(r.tokens[0].type, TokenType::option); EXPECT_EQ(r.tokens[0].text, "verbose"); - EXPECT_EQ(r.tokens[0].matched_prefix, "--"); + EXPECT_EQ(cli::token_prefix_view(r.tokens[0], {}), "--"); } TEST(Tokenize, FlagDoesNotConsumeNextToken) { @@ -113,7 +126,7 @@ TEST(Tokenize, OptionExactlyOneValue) { ASSERT_EQ(r.tokens.size(), 2u); EXPECT_EQ(r.tokens[0].type, TokenType::option); EXPECT_EQ(r.tokens[0].text, "name"); - EXPECT_EQ(r.tokens[0].matched_prefix, "--"); + EXPECT_EQ(cli::token_prefix_view(r.tokens[0], {}), "--"); EXPECT_EQ(r.tokens[0].position, 0u); EXPECT_EQ(r.tokens[1].type, TokenType::value); EXPECT_EQ(r.tokens[1].text, "Alice"); @@ -176,7 +189,7 @@ TEST(Tokenize, InlineSyntaxSplitsIntoOptionAndValueAtSamePosition) { ASSERT_EQ(r.tokens.size(), 2u); EXPECT_EQ(r.tokens[0].type, TokenType::option); EXPECT_EQ(r.tokens[0].text, "name"); - EXPECT_EQ(r.tokens[0].matched_prefix, "--"); + EXPECT_EQ(cli::token_prefix_view(r.tokens[0], {}), "--"); EXPECT_EQ(r.tokens[0].position, 0u); EXPECT_EQ(r.tokens[1].type, TokenType::value); EXPECT_EQ(r.tokens[1].text, "Alice"); @@ -443,42 +456,53 @@ TEST(Tokenize, ErrorInlineSyntaxCannotSatisfyMinTwo) { static auto tokenize_cfg(std::initializer_list args, const SpecMap& spec, const cli::TokenizerConfig& cfg, const CmdSet& cmds = {}) -> TokenizeResult { - return cli::tokenize(tok(args), spec, cmds, cfg); + auto option_nargs = [&](std::string_view opt) -> std::optional { + if (auto it = spec.find(std::string(opt)); it != spec.end()) { + return it->second; + } + return std::nullopt; + }; + + auto is_command = [&](std::string_view name) -> bool { + return cmds.contains(std::string(name)); + }; + + return cli::tokenize(tok(args), option_nargs, is_command, cfg); } TEST(TokenizeMultiPrefix, AltPrefixRecognised) { // "+verbose" recognised as option when "+" is in option_prefixes - cli::TokenizerConfig cfg{.option_prefixes = {"--", "+"}}; + auto cfg = cli::TokenizerConfig{}.with_option_prefixes({"--", "+"}); auto r = tokenize_cfg({"+verbose"}, {{"+verbose", kFlag}}, cfg); ASSERT_TRUE(r.has_value()); ASSERT_EQ(r.tokens.size(), 1u); EXPECT_EQ(r.tokens[0].type, TokenType::option); EXPECT_EQ(r.tokens[0].text, "verbose"); - EXPECT_EQ(r.tokens[0].matched_prefix, "+"); + EXPECT_EQ(cli::token_prefix_view(r.tokens[0], cfg), "+"); } TEST(TokenizeMultiPrefix, MatchedPrefixDistinguishesPrefixes) { // "--foo" and "+foo" are different options; matched_prefix tells them apart - cli::TokenizerConfig cfg{.option_prefixes = {"--", "+"}}; + auto cfg = cli::TokenizerConfig{}.with_option_prefixes({"--", "+"}); SpecMap spec{{"--foo", kFlag}, {"+foo", kFlag}}; auto r = tokenize_cfg({"--foo", "+foo"}, spec, cfg); ASSERT_TRUE(r.has_value()); ASSERT_EQ(r.tokens.size(), 2u); - EXPECT_EQ(r.tokens[0].matched_prefix, "--"); - EXPECT_EQ(r.tokens[1].matched_prefix, "+"); + EXPECT_EQ(cli::token_prefix_view(r.tokens[0], cfg), "--"); + EXPECT_EQ(cli::token_prefix_view(r.tokens[1], cfg), "+"); } TEST(TokenizeMultiPrefix, DefaultPrefixStillWorks) { // Default config ({"--"}) behaves identically to before auto r = tokenize_cfg({"--name", "Alice"}, {{"--name", kOne}}, {}); ASSERT_TRUE(r.has_value()); - EXPECT_EQ(r.tokens[0].matched_prefix, "--"); + EXPECT_EQ(cli::token_prefix_view(r.tokens[0], {}), "--"); EXPECT_EQ(r.tokens[1].text, "Alice"); - EXPECT_TRUE(r.tokens[1].matched_prefix.empty()); + EXPECT_TRUE(cli::token_prefix_view(r.tokens[1], {}).empty()); } TEST(TokenizeMultiPrefix, UnknownAltPrefixTokenIsError) { - cli::TokenizerConfig cfg{.option_prefixes = {"--", "+"}}; + auto cfg = cli::TokenizerConfig{}.with_option_prefixes({"--", "+"}); auto r = tokenize_cfg({"+unknown"}, {{"--foo", kFlag}}, cfg); ASSERT_FALSE(r.has_value()); EXPECT_EQ(r.error.code, ErrorCode::unknown_option); @@ -488,7 +512,7 @@ TEST(TokenizeMultiPrefix, UnknownAltPrefixTokenIsError) { // ---- inline_value_separator ----------------------------------------------- TEST(TokenizeInlineSep, ColonSeparator) { - cli::TokenizerConfig cfg{.inline_value_separator = ':'}; + auto cfg = cli::TokenizerConfig{}.with_inline_value_separator(':'); auto r = tokenize_cfg({"--name:Alice"}, {{"--name", kOne}}, cfg); ASSERT_TRUE(r.has_value()); ASSERT_EQ(r.tokens.size(), 2u); @@ -505,7 +529,7 @@ TEST(TokenizeInlineSep, DefaultEqualsStillWorks) { TEST(TokenizeInlineSep, EqualsTreatedAsValueWhenSepIsColon) { // With sep=':', "--name=Alice" has no separator → entire token is option name. - cli::TokenizerConfig cfg{.inline_value_separator = ':'}; + auto cfg = cli::TokenizerConfig{}.with_inline_value_separator(':'); auto r = tokenize_cfg({"--name=Alice"}, {{"--name=Alice", kFlag}}, cfg); ASSERT_TRUE(r.has_value()); EXPECT_EQ(r.tokens[0].text, "name=Alice"); @@ -531,7 +555,7 @@ TEST(TokenizeCluster, SingleFlagStillWorks) { ASSERT_TRUE(r.has_value()); ASSERT_EQ(r.tokens.size(), 1u); EXPECT_EQ(r.tokens[0].text, "v"); - EXPECT_EQ(r.tokens[0].matched_prefix, "-"); + EXPECT_EQ(cli::token_prefix_view(r.tokens[0], {}), "-"); } TEST(TokenizeCluster, FlagsThenValueNextToken) { @@ -551,7 +575,7 @@ TEST(TokenizeCluster, AttachedValueOnly) { ASSERT_TRUE(r.has_value()); ASSERT_EQ(r.tokens.size(), 2u); EXPECT_EQ(r.tokens[0].text, "o"); - EXPECT_EQ(r.tokens[0].matched_prefix, "-"); + EXPECT_EQ(cli::token_prefix_view(r.tokens[0], {}), "-"); EXPECT_EQ(r.tokens[1].type, TokenType::value); EXPECT_EQ(r.tokens[1].text, "file"); EXPECT_EQ(r.tokens[1].position, 0u); @@ -643,7 +667,7 @@ TEST(Tokenize, ShortOptionInlineSeparator) { ASSERT_EQ(r.tokens.size(), 2u); EXPECT_EQ(r.tokens[0].type, TokenType::option); EXPECT_EQ(r.tokens[0].text, "o"); - EXPECT_EQ(r.tokens[0].matched_prefix, "-"); + EXPECT_EQ(cli::token_prefix_view(r.tokens[0], {}), "-"); EXPECT_EQ(r.tokens[1].type, TokenType::value); EXPECT_EQ(r.tokens[1].text, "file"); } From c7de40b84ebf12d113001f633aa6df6d3cf202f7 Mon Sep 17 00:00:00 2001 From: gen740 <54583542+gen740@users.noreply.github.com> Date: Wed, 13 May 2026 00:44:23 +0900 Subject: [PATCH 07/15] fix(bench): ensure portable script execution and Nix compatibility Invoke benchmark scripts using explicit interpreters and switch google-benchmark to libcxxStdenv in the Nix flake for better C++ standard support and environment consistency. --- bench/compile_time/scripts/run.sh | 2 +- bench/scripts/summarize.py | 1 - flake.nix | 10 +++++----- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/bench/compile_time/scripts/run.sh b/bench/compile_time/scripts/run.sh index 4819136..ae1789a 100755 --- a/bench/compile_time/scripts/run.sh +++ b/bench/compile_time/scripts/run.sh @@ -26,7 +26,7 @@ for case_name in "${cases[@]}"; do --runs "$hyperfine_runs" \ --prepare "rm -f '$object_file'" \ --export-json "$json_file" \ - "$repo_root/bench/compile_time/scripts/compile-one.sh '$case_name' '$library' '$object_dir'" \ + "bash $repo_root/bench/compile_time/scripts/compile-one.sh '$case_name' '$library' '$object_dir'" \ >/dev/null elapsed="$(python3 -c 'import json, sys; print("{:.6f}".format(json.load(open(sys.argv[1]))["results"][0]["mean"]))' "$json_file")" printf "c++%s\t%s\t%s\t%s\n" "$cxx_standard" "$case_name" "$library" "$elapsed" | diff --git a/bench/scripts/summarize.py b/bench/scripts/summarize.py index 77a1034..c63578c 100755 --- a/bench/scripts/summarize.py +++ b/bench/scripts/summarize.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 from __future__ import annotations import csv diff --git a/flake.nix b/flake.nix index 612d5e0..a531ee0 100644 --- a/flake.nix +++ b/flake.nix @@ -30,7 +30,7 @@ { pkgs, config, ... }: let version = builtins.replaceStrings [ "\n" ] [ "" ] (builtins.readFile ./VERSION); - google_bench = pkgs.stdenv.mkDerivation rec { + google_bench = pkgs.libcxxStdenv.mkDerivation rec { pname = "google-benchmark"; version = "1.9.5"; @@ -194,11 +194,11 @@ for standard in 20 26; do export BENCH_CXX_STANDARD="$standard" - bench/scripts/run-runtime.sh --benchmark_min_time=0.01s - bench/scripts/run-size.sh - bench/compile_time/scripts/run.sh "$RESULTS_DIR/compile_time/cxx$standard" + bash bench/scripts/run-runtime.sh --benchmark_min_time=0.01s + bash bench/scripts/run-size.sh + bash bench/compile_time/scripts/run.sh "$RESULTS_DIR/compile_time/cxx$standard" done - bench/scripts/summarize.py --output "$RESULTS_DIR/report.md" + python3 bench/scripts/summarize.py --output "$RESULTS_DIR/report.md" runHook postBuild ''; From e840f9cd5c1cbc2c1dce0eacf869e795034701ce Mon Sep 17 00:00:00 2001 From: gen740 <54583542+gen740@users.noreply.github.com> Date: Wed, 13 May 2026 01:20:45 +0900 Subject: [PATCH 08/15] Add Boost benchmarks and benchmark docs --- bench/cmake/BenchLibraries.cmake | 7 + .../compile_time/cases/many_options/boost.cc | 1 + bench/compile_time/cases/simple/boost.cc | 1 + bench/compile_time/cases/subcommand/boost.cc | 1 + bench/compile_time/scripts/run.sh | 2 +- bench/runtime/CMakeLists.txt | 1 + bench/runtime/many_options.cc | 62 ++ bench/runtime/simple.cc | 36 + bench/runtime/subcommand.cc | 30 + bench/scripts/export-doc-data.py | 133 ++++ bench/size/CMakeLists.txt | 4 +- bench/size/many_options/boost.cc | 79 ++ bench/size/simple/boost.cc | 28 + bench/size/subcommand/boost.cc | 36 + docs/docs/benchmarks.mdx | 48 ++ docs/sidebars.ts | 1 + docs/src/components/BenchmarkCharts.jsx | 285 ++++++++ docs/src/css/custom.css | 159 +++- docs/src/data/benchmarks/results.json | 685 ++++++++++++++++++ flake.nix | 21 +- 20 files changed, 1616 insertions(+), 4 deletions(-) create mode 100644 bench/compile_time/cases/many_options/boost.cc create mode 100644 bench/compile_time/cases/simple/boost.cc create mode 100644 bench/compile_time/cases/subcommand/boost.cc create mode 100644 bench/scripts/export-doc-data.py create mode 100644 bench/size/many_options/boost.cc create mode 100644 bench/size/simple/boost.cc create mode 100644 bench/size/subcommand/boost.cc create mode 100644 docs/docs/benchmarks.mdx create mode 100644 docs/src/components/BenchmarkCharts.jsx create mode 100644 docs/src/data/benchmarks/results.json diff --git a/bench/cmake/BenchLibraries.cmake b/bench/cmake/BenchLibraries.cmake index e1efc30..b5fc372 100644 --- a/bench/cmake/BenchLibraries.cmake +++ b/bench/cmake/BenchLibraries.cmake @@ -15,6 +15,10 @@ target_compile_options(bench_warnings INTERFACE ) find_package(benchmark REQUIRED) +if(POLICY CMP0167) + cmake_policy(SET CMP0167 NEW) +endif() +find_package(Boost REQUIRED COMPONENTS program_options) find_path(BENCH_CLI11_INCLUDE_DIR CLI/CLI.hpp REQUIRED) find_path(BENCH_ARGPARSE_INCLUDE_DIR argparse/argparse.hpp REQUIRED) @@ -29,6 +33,9 @@ target_include_directories(bench_argparse INTERFACE "${BENCH_ARGPARSE_INCLUDE_DI add_library(bench_cxxopts INTERFACE) target_include_directories(bench_cxxopts INTERFACE "${BENCH_CXXOPTS_INCLUDE_DIR}") +add_library(bench_boost_program_options INTERFACE) +target_link_libraries(bench_boost_program_options INTERFACE Boost::program_options) + function(bench_add_size_executable target source library_target) add_executable("${target}" "${source}") target_link_libraries("${target}" PRIVATE diff --git a/bench/compile_time/cases/many_options/boost.cc b/bench/compile_time/cases/many_options/boost.cc new file mode 100644 index 0000000..512daf2 --- /dev/null +++ b/bench/compile_time/cases/many_options/boost.cc @@ -0,0 +1 @@ +#include "../../../size/many_options/boost.cc" diff --git a/bench/compile_time/cases/simple/boost.cc b/bench/compile_time/cases/simple/boost.cc new file mode 100644 index 0000000..9838f59 --- /dev/null +++ b/bench/compile_time/cases/simple/boost.cc @@ -0,0 +1 @@ +#include "../../../size/simple/boost.cc" diff --git a/bench/compile_time/cases/subcommand/boost.cc b/bench/compile_time/cases/subcommand/boost.cc new file mode 100644 index 0000000..0778f0a --- /dev/null +++ b/bench/compile_time/cases/subcommand/boost.cc @@ -0,0 +1 @@ +#include "../../../size/subcommand/boost.cc" diff --git a/bench/compile_time/scripts/run.sh b/bench/compile_time/scripts/run.sh index ae1789a..59ec134 100755 --- a/bench/compile_time/scripts/run.sh +++ b/bench/compile_time/scripts/run.sh @@ -8,7 +8,7 @@ hyperfine_runs="${HYPERFINE_RUNS:-5}" hyperfine_warmup="${HYPERFINE_WARMUP:-1}" cases=(simple many_options subcommand) -libraries=(cli20 cli11 argparse cxxopts) +libraries=(cli20 cli11 argparse cxxopts boost) mkdir -p "$out_dir" result_file="$out_dir/results.tsv" diff --git a/bench/runtime/CMakeLists.txt b/bench/runtime/CMakeLists.txt index 519e64c..509d201 100644 --- a/bench/runtime/CMakeLists.txt +++ b/bench/runtime/CMakeLists.txt @@ -11,6 +11,7 @@ target_link_libraries(bench_runtime PRIVATE bench_cli11 bench_argparse bench_cxxopts + bench_boost_program_options benchmark::benchmark benchmark::benchmark_main ) diff --git a/bench/runtime/many_options.cc b/bench/runtime/many_options.cc index 6a72002..c8e5923 100644 --- a/bench/runtime/many_options.cc +++ b/bench/runtime/many_options.cc @@ -2,6 +2,9 @@ #include #include +#include +#include +#include #include #include #include @@ -18,6 +21,8 @@ namespace bench { +namespace po = boost::program_options; + enum class Mode { fast, balanced, precise }; inline auto mode_name(Mode mode) -> std::string_view { @@ -44,6 +49,17 @@ inline auto parse_mode_or_throw(const std::string& value) -> Mode { throw std::runtime_error("invalid mode: " + value); } +inline void validate(boost::any& out, const std::vector& values, + Mode*, int) { + po::validators::check_first_occurrence(out); + const auto& value = po::validators::get_single_string(values); + if (auto mode = parse_mode(value)) { + out = *mode; + return; + } + throw po::validation_error(po::validation_error::invalid_option_value, value); +} + inline auto operator>>(std::istream& in, Mode& mode) -> std::istream& { std::string value; in >> value; @@ -184,6 +200,27 @@ void add_cxxopts_options(cxxopts::Options& options) { "color", "color", cxxopts::value()); } +void add_boost_options(boost::program_options::options_description& options, + bench::ManyValues& values) { + namespace po = boost::program_options; + options.add_options()("name", po::value(&values.name))( + "output", po::value(&values.output))( + "target", po::value(&values.target))( + "define", po::value(&values.define))( + "jobs", po::value(&values.jobs))( + "retries", po::value(&values.retries))("port", + po::value(&values.port))( + "depth", po::value(&values.depth))("ratio", + po::value(&values.ratio))( + "timeout", po::value(&values.timeout))( + "threshold", po::value(&values.threshold))( + "scale", po::value(&values.scale))( + "config", po::value(&values.config))( + "cache", po::value(&values.cache))( + "mode", po::value(&values.mode))( + "color", po::value(&values.color)); +} + void BM_Cli20ManyOptions(benchmark::State& state) { auto& args = many_argv(); std::size_t allocations = 0; @@ -251,9 +288,34 @@ void BM_CxxoptsManyOptions(benchmark::State& state) { bench::set_allocation_counters(state, allocations, bytes); } +void BM_BoostManyOptions(benchmark::State& state) { + auto& args = many_argv(); + std::size_t allocations = 0; + std::size_t bytes = 0; + for (auto _ : state) { + const auto stats = bench::measure_allocations([&] { + boost::program_options::options_description options{"many_options"}; + bench::ManyValues values; + add_boost_options(options, values); + boost::program_options::variables_map parsed; + boost::program_options::store( + boost::program_options::command_line_parser(args.argc(), args.argv()) + .options(options) + .run(), + parsed); + boost::program_options::notify(parsed); + benchmark::DoNotOptimize(values); + }); + allocations += stats.allocations; + bytes += stats.bytes; + } + bench::set_allocation_counters(state, allocations, bytes); +} + BENCHMARK(BM_Cli20ManyOptions); BENCHMARK(BM_Cli11ManyOptions); BENCHMARK(BM_ArgparseManyOptions); BENCHMARK(BM_CxxoptsManyOptions); +BENCHMARK(BM_BoostManyOptions); } // namespace diff --git a/bench/runtime/simple.cc b/bench/runtime/simple.cc index f0ac1c9..1454d3f 100644 --- a/bench/runtime/simple.cc +++ b/bench/runtime/simple.cc @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -12,6 +13,8 @@ namespace { +namespace po = boost::program_options; + struct Cli20Simple { cli::Flag<"verbose", 'v'> verbose; cli::StringOption<"output", 'o'> output; @@ -102,9 +105,42 @@ void BM_CxxoptsSimple(benchmark::State& state) { bench::set_allocation_counters(state, allocations, bytes); } +void BM_BoostSimple(benchmark::State& state) { + auto& args = simple_argv(); + std::size_t allocations = 0; + std::size_t bytes = 0; + for (auto _ : state) { + const auto stats = bench::measure_allocations([&] { + bool verbose = false; + std::string output; + std::string input; + po::options_description options{"simple"}; + options.add_options()("verbose,v", po::bool_switch(&verbose))( + "output,o", po::value(&output))( + "input", po::value(&input)->required()); + po::positional_options_description positional; + positional.add("input", 1); + po::variables_map values; + po::store(po::command_line_parser(args.argc(), args.argv()) + .options(options) + .positional(positional) + .run(), + values); + po::notify(values); + benchmark::DoNotOptimize(verbose); + benchmark::DoNotOptimize(output); + benchmark::DoNotOptimize(input); + }); + allocations += stats.allocations; + bytes += stats.bytes; + } + bench::set_allocation_counters(state, allocations, bytes); +} + BENCHMARK(BM_Cli20Simple); BENCHMARK(BM_Cli11Simple); BENCHMARK(BM_ArgparseSimple); BENCHMARK(BM_CxxoptsSimple); +BENCHMARK(BM_BoostSimple); } // namespace diff --git a/bench/runtime/subcommand.cc b/bench/runtime/subcommand.cc index 7ac21c2..f8b9a48 100644 --- a/bench/runtime/subcommand.cc +++ b/bench/runtime/subcommand.cc @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -12,6 +13,8 @@ namespace { +namespace po = boost::program_options; + struct Cli20Build { cli::StringOption<"profile"> profile; }; @@ -111,9 +114,36 @@ void BM_CxxoptsSubcommand(benchmark::State& state) { bench::set_allocation_counters(state, allocations, bytes); } +void BM_BoostSubcommand(benchmark::State& state) { + auto& args = subcommand_argv(); + std::size_t allocations = 0; + std::size_t bytes = 0; + for (auto _ : state) { + const auto stats = bench::measure_allocations([&] { + const std::string command = args.storage[1]; + if (command == "build") { + std::string profile; + po::options_description options{"build"}; + options.add_options()("profile", po::value(&profile)); + po::variables_map values; + po::store(po::command_line_parser(args.argc() - 1, args.argv() + 1) + .options(options) + .run(), + values); + po::notify(values); + benchmark::DoNotOptimize(profile); + } + }); + allocations += stats.allocations; + bytes += stats.bytes; + } + bench::set_allocation_counters(state, allocations, bytes); +} + BENCHMARK(BM_Cli20Subcommand); BENCHMARK(BM_Cli11Subcommand); BENCHMARK(BM_ArgparseSubcommand); BENCHMARK(BM_CxxoptsSubcommand); +BENCHMARK(BM_BoostSubcommand); } // namespace diff --git a/bench/scripts/export-doc-data.py b/bench/scripts/export-doc-data.py new file mode 100644 index 0000000..ed85ee9 --- /dev/null +++ b/bench/scripts/export-doc-data.py @@ -0,0 +1,133 @@ +from __future__ import annotations + +import csv +import json +import re +import sys +from pathlib import Path + + +LIBRARY_NAMES = { + "Cli20": "cli20", + "Cli11": "cli11", + "Argparse": "argparse", + "Cxxopts": "cxxopts", + "Boost": "boost", +} + +CASE_NAMES = { + "Simple": "simple", + "ManyOptions": "many_options", + "Subcommand": "subcommand", +} + +RUNTIME_RE = re.compile( + r"^BM_(Cli20|Cli11|Argparse|Cxxopts|Boost)(Simple|ManyOptions|Subcommand)$" +) + + +def read_runtime(path: Path, standard: str) -> list[dict[str, object]]: + data = json.loads(path.read_text()) + rows: list[dict[str, object]] = [] + for bench in data.get("benchmarks", []): + match = RUNTIME_RE.match(str(bench.get("name", ""))) + if not match: + continue + library_name, case_name = match.groups() + rows.append( + { + "standard": standard, + "case": CASE_NAMES[case_name], + "library": LIBRARY_NAMES[library_name], + "timeNs": bench.get("real_time", 0), + "cpuNs": bench.get("cpu_time", 0), + "allocations": bench.get("allocs", 0), + "allocatedBytes": bench.get("bytes", 0), + } + ) + return rows + + +def read_size(path: Path) -> list[dict[str, object]]: + if not path.exists(): + return [] + with path.open(newline="") as file: + rows = csv.DictReader(file, delimiter="\t") + return [ + { + "standard": row["standard"].replace("++", "xx"), + "case": row["case"], + "library": row["library"], + "unstrippedBytes": int(row["unstripped_bytes"]), + "strippedBytes": int(row["stripped_bytes"]), + } + for row in rows + if row.get("stripped_bytes", "").isdigit() + ] + + +def read_compile_time(path: Path) -> list[dict[str, object]]: + if not path.exists(): + return [] + with path.open(newline="") as file: + rows = csv.DictReader(file, delimiter="\t") + return [ + { + "standard": row["standard"].replace("++", "xx"), + "case": row["case"], + "library": row["library"], + "seconds": float(row["seconds"]), + } + for row in rows + ] + + +def collect(result_dir: Path) -> dict[str, object]: + runtime_rows: list[dict[str, object]] = [] + size_rows: list[dict[str, object]] = [] + compile_rows: list[dict[str, object]] = [] + + for runtime_path in sorted(result_dir.glob("runtime_cxx*.json")): + standard = runtime_path.stem.removeprefix("runtime_") + runtime_rows.extend(read_runtime(runtime_path, standard)) + + for size_path in sorted(result_dir.glob("size_cxx*.tsv")): + size_rows.extend(read_size(size_path)) + + for compile_path in sorted(result_dir.glob("compile_time/cxx*/results.tsv")): + compile_rows.extend(read_compile_time(compile_path)) + + standards = sorted( + { + str(row["standard"]) + for row in [*runtime_rows, *size_rows, *compile_rows] + } + ) + cases = ["simple", "many_options", "subcommand"] + libraries = ["cli20", "cli11", "argparse", "cxxopts", "boost"] + + return { + "schemaVersion": 1, + "standards": standards, + "cases": cases, + "libraries": libraries, + "runtime": runtime_rows, + "size": size_rows, + "compileTime": compile_rows, + } + + +def main() -> None: + if len(sys.argv) != 3: + print("usage: export-doc-data.py RESULT_DIR OUTPUT_JSON", file=sys.stderr) + raise SystemExit(2) + + result_dir = Path(sys.argv[1]).resolve() + output = Path(sys.argv[2]).resolve() + data = collect(result_dir) + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n") + + +if __name__ == "__main__": + main() diff --git a/bench/size/CMakeLists.txt b/bench/size/CMakeLists.txt index 90a8d2d..d785833 100644 --- a/bench/size/CMakeLists.txt +++ b/bench/size/CMakeLists.txt @@ -1,5 +1,5 @@ set(BENCH_SIZE_CASES simple many_options subcommand) -set(BENCH_SIZE_LIBS cli20 cli11 argparse cxxopts) +set(BENCH_SIZE_LIBS cli20 cli11 argparse cxxopts boost) foreach(case_name IN LISTS BENCH_SIZE_CASES) foreach(lib_name IN LISTS BENCH_SIZE_LIBS) @@ -13,6 +13,8 @@ foreach(case_name IN LISTS BENCH_SIZE_CASES) set(link_target bench_argparse) elseif(lib_name STREQUAL "cxxopts") set(link_target bench_cxxopts) + elseif(lib_name STREQUAL "boost") + set(link_target bench_boost_program_options) endif() bench_add_size_executable("${target_name}" "${source_file}" "${link_target}") endforeach() diff --git a/bench/size/many_options/boost.cc b/bench/size/many_options/boost.cc new file mode 100644 index 0000000..fafc816 --- /dev/null +++ b/bench/size/many_options/boost.cc @@ -0,0 +1,79 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +namespace po = boost::program_options; + +enum class Mode { fast, balanced, precise }; + +inline auto parse_mode(std::string_view value) -> std::optional { + if (value == "fast") return Mode::fast; + if (value == "balanced") return Mode::balanced; + if (value == "precise") return Mode::precise; + return std::nullopt; +} + +inline void validate(boost::any& out, const std::vector& values, + Mode*, int) { + po::validators::check_first_occurrence(out); + const auto& value = po::validators::get_single_string(values); + if (auto mode = parse_mode(value)) { + out = *mode; + return; + } + throw po::validation_error(po::validation_error::invalid_option_value, value); +} + +struct ManyValues { + std::string name; + std::string output; + std::string target; + std::string define; + int jobs{}; + int retries{}; + int port{}; + int depth{}; + double ratio{}; + double timeout{}; + double threshold{}; + double scale{}; + std::filesystem::path config; + std::filesystem::path cache; + Mode mode{}; + Mode color{}; +}; + +auto main(int argc, char** argv) -> int { + po::options_description options{"many_options"}; + ManyValues values; + options.add_options()("name", po::value(&values.name))( + "output", po::value(&values.output))( + "target", po::value(&values.target))( + "define", po::value(&values.define))( + "jobs", po::value(&values.jobs))( + "retries", po::value(&values.retries))("port", + po::value(&values.port))( + "depth", po::value(&values.depth))("ratio", + po::value(&values.ratio))( + "timeout", po::value(&values.timeout))( + "threshold", po::value(&values.threshold))( + "scale", po::value(&values.scale))( + "config", po::value(&values.config))( + "cache", po::value(&values.cache))( + "mode", po::value(&values.mode))("color", + po::value(&values.color)); + try { + po::variables_map parsed; + po::store(po::command_line_parser(argc, argv).options(options).run(), + parsed); + po::notify(parsed); + } catch (const po::error&) { + return 1; + } + return 0; +} diff --git a/bench/size/simple/boost.cc b/bench/size/simple/boost.cc new file mode 100644 index 0000000..6175fcc --- /dev/null +++ b/bench/size/simple/boost.cc @@ -0,0 +1,28 @@ +#include +#include + +namespace po = boost::program_options; + +auto main(int argc, char** argv) -> int { + bool verbose = false; + std::string output; + std::string input; + po::options_description options{"simple"}; + options.add_options()("verbose,v", po::bool_switch(&verbose))( + "output,o", po::value(&output))( + "input", po::value(&input)->required()); + po::positional_options_description positional; + positional.add("input", 1); + try { + po::variables_map values; + po::store(po::command_line_parser(argc, argv) + .options(options) + .positional(positional) + .run(), + values); + po::notify(values); + } catch (const po::error&) { + return 1; + } + return 0; +} diff --git a/bench/size/subcommand/boost.cc b/bench/size/subcommand/boost.cc new file mode 100644 index 0000000..5cb9231 --- /dev/null +++ b/bench/size/subcommand/boost.cc @@ -0,0 +1,36 @@ +#include +#include + +namespace po = boost::program_options; + +auto main(int argc, char** argv) -> int { + if (argc < 2) return 1; + const std::string command = argv[1]; + try { + if (command == "build") { + std::string profile; + po::options_description options{"build"}; + options.add_options()("profile", po::value(&profile)); + po::variables_map values; + po::store( + po::command_line_parser(argc - 1, argv + 1).options(options).run(), + values); + po::notify(values); + return 0; + } + if (command == "test") { + bool verbose = false; + po::options_description options{"test"}; + options.add_options()("verbose,v", po::bool_switch(&verbose)); + po::variables_map values; + po::store( + po::command_line_parser(argc - 1, argv + 1).options(options).run(), + values); + po::notify(values); + return 0; + } + } catch (const po::error&) { + return 1; + } + return 1; +} diff --git a/docs/docs/benchmarks.mdx b/docs/docs/benchmarks.mdx new file mode 100644 index 0000000..c280826 --- /dev/null +++ b/docs/docs/benchmarks.mdx @@ -0,0 +1,48 @@ +--- +sidebar_position: 5 +title: Benchmarks +--- + +import BenchmarkCharts from '@site/src/components/BenchmarkCharts'; + +# Benchmarks + +This page compares cli20 against CLI11, argparse, cxxopts, and +Boost.Program_options across three representative schemas. All benchmark +metrics on this page are **smaller is better**. + +- `simple`: one flag, one option, and one required positional argument. +- `many_options`: strings, integers, doubles, paths, and enum-like choices. +- `subcommand`: a small command tree with `build` and `test`. + +Boost.Program_options is shown as a reference value because it links against +the Boost.Program_options shared library. Its binary-size numbers are therefore +not directly comparable with the mostly header-only libraries. + +## Results + + + +## Benchmark Source + +The benchmark code is kept in the repository so every number on this page can +be traced back to the parser setup being measured. + +| Area | Source | +|---|---| +| Runtime benchmarks | [`bench/runtime/`](https://github.com/CLI20-dev/cli20/tree/main/bench/runtime) | +| Binary size benchmarks | [`bench/size/`](https://github.com/CLI20-dev/cli20/tree/main/bench/size) | +| Compile-time benchmarks | [`bench/compile_time/`](https://github.com/CLI20-dev/cli20/tree/main/bench/compile_time) | +| Shared argv/allocation helpers | [`bench/include/bench/`](https://github.com/CLI20-dev/cli20/tree/main/bench/include/bench) | +| Result summarization | [`bench/scripts/`](https://github.com/CLI20-dev/cli20/tree/main/bench/scripts) | + +## Method + +Runtime benchmarks construct the parser and parse the same argv every +iteration. Allocation counts are measured inside the same operation. + +Binary size benchmarks build one executable per library and case, then record +both the unstripped and stripped file sizes. + +Compile-time benchmarks compile one translation unit per library and case with +`hyperfine`, using the same benchmark source as the binary-size targets. diff --git a/docs/sidebars.ts b/docs/sidebars.ts index 2880a57..9d92699 100644 --- a/docs/sidebars.ts +++ b/docs/sidebars.ts @@ -6,6 +6,7 @@ const sidebars: SidebarsConfig = { 'tutorial', 'actions', 'relations', + 'benchmarks', { type: 'category', label: 'Examples', diff --git a/docs/src/components/BenchmarkCharts.jsx b/docs/src/components/BenchmarkCharts.jsx new file mode 100644 index 0000000..0989813 --- /dev/null +++ b/docs/src/components/BenchmarkCharts.jsx @@ -0,0 +1,285 @@ +import React, {useMemo, useState} from 'react'; +import results from '@site/src/data/benchmarks/results.json'; + +const caseLabels = { + simple: 'Simple', + many_options: 'Many options', + subcommand: 'Subcommand', +}; + +const caseColors = { + simple: '#2563eb', + many_options: '#b45309', + subcommand: '#0f766e', +}; + +const caseDash = { + simple: '', + many_options: '7 4', + subcommand: '2 4', +}; + +const libraryLabels = { + cli20: 'cli20', + cli11: 'CLI11', + argparse: 'argparse', + cxxopts: 'cxxopts', + boost: 'Boost*', +}; + +function number(value) { + return new Intl.NumberFormat('en', { + maximumFractionDigits: value < 10 ? 2 : 1, + }).format(value); +} + +function bytes(value) { + if (value >= 1024 * 1024) return `${number(value / (1024 * 1024))} MiB`; + if (value >= 1024) return `${number(value / 1024)} KiB`; + return `${number(value)} B`; +} + +function metricValue(row, metric) { + if (metric === 'runtime') return row.timeNs; + if (metric === 'allocations') return row.allocations; + if (metric === 'allocatedBytes') return row.allocatedBytes; + if (metric === 'binarySize') return row.strippedBytes; + if (metric === 'compileTime') return row.seconds; + return 0; +} + +function formatMetric(value, metric) { + if (metric === 'runtime') return `${number(value)} ns`; + if (metric === 'allocations') return `${number(value)} allocs`; + if (metric === 'allocatedBytes' || metric === 'binarySize') return bytes(value); + if (metric === 'compileTime') return `${number(value)} s`; + return number(value); +} + +function rowsFor(metric) { + if (metric === 'binarySize') return results.size; + if (metric === 'compileTime') return results.compileTime; + return results.runtime; +} + +function LineChart({title, metric, standard}) { + const rows = rowsFor(metric).filter((row) => row.standard === standard); + const max = Math.max(...rows.map((row) => metricValue(row, metric)), 1); + const width = 760; + const height = 320; + const padding = {top: 18, right: 28, bottom: 68, left: 82}; + const plotWidth = width - padding.left - padding.right; + const plotHeight = height - padding.top - padding.bottom; + const xFor = (index) => + padding.left + (plotWidth * index) / Math.max(results.libraries.length - 1, 1); + const yFor = (value) => padding.top + plotHeight - (value / max) * plotHeight; + const ticks = [0, 0.25, 0.5, 0.75, 1]; + + return ( +
+
+

{title}

+ smaller is better +
+
+ + {ticks.map((tick) => { + const value = max * tick; + const y = yFor(value); + return ( + + + + {formatMetric(value, metric)} + + + ); + })} + + + {results.libraries.map((library, index) => ( + + + + {libraryLabels[library] ?? library} + + + ))} + {results.cases.map((caseName) => { + const points = results.libraries + .map((library, index) => { + const row = rows.find((item) => item.case === caseName && item.library === library); + if (!row) return null; + return { + x: xFor(index), + y: yFor(metricValue(row, metric)), + value: metricValue(row, metric), + library, + }; + }) + .filter(Boolean); + const path = points.map((point, index) => `${index === 0 ? 'M' : 'L'} ${point.x} ${point.y}`).join(' '); + return ( + + + {points.map((point) => ( + + + + {`${caseLabels[caseName] ?? caseName} / ${ + libraryLabels[point.library] ?? point.library + }: ${formatMetric(point.value, metric)}`} + + + ))} + + ); + })} + +
+ {results.cases.map((caseName) => ( + + + {caseLabels[caseName] ?? caseName} + + ))} +
+
+
+ ); +} + +function ResultTable({standard}) { + const rows = useMemo(() => { + return results.runtime + .filter((row) => row.standard === standard) + .map((runtimeRow) => { + const sizeRow = results.size.find( + (row) => + row.standard === standard && + row.case === runtimeRow.case && + row.library === runtimeRow.library, + ); + const compileRow = results.compileTime.find( + (row) => + row.standard === standard && + row.case === runtimeRow.case && + row.library === runtimeRow.library, + ); + return {runtimeRow, sizeRow, compileRow}; + }); + }, [standard]); + + return ( +
+ + + + + + + + + + + + + + {rows.map(({runtimeRow, sizeRow, compileRow}) => ( + + + + + + + + + + ))} + +
CaseLibraryRuntimeAllocsBytesStripped sizeCompile time
{caseLabels[runtimeRow.case] ?? runtimeRow.case}{libraryLabels[runtimeRow.library] ?? runtimeRow.library}{formatMetric(runtimeRow.timeNs, 'runtime')}{formatMetric(runtimeRow.allocations, 'allocations')}{formatMetric(runtimeRow.allocatedBytes, 'allocatedBytes')}{sizeRow ? formatMetric(sizeRow.strippedBytes, 'binarySize') : '-'}{compileRow ? formatMetric(compileRow.seconds, 'compileTime') : '-'}
+
+ ); +} + +export default function BenchmarkCharts() { + const standards = results.standards.length > 0 ? results.standards : ['cxx20']; + const [standard, setStandard] = useState(standards[0]); + + if (results.runtime.length === 0) { + return ( +
+ Benchmark data has not been generated yet. +
+ ); + } + + return ( +
+
+ Language mode + {standards.map((item) => ( + + ))} +
+ + + + + +

+ * Boost.Program_options is a reference value: it links against the + Boost.Program_options shared library, so binary-size results do not + represent a fully self-contained executable in the same way as the + header-oriented libraries. +

+ +
+ ); +} diff --git a/docs/src/css/custom.css b/docs/src/css/custom.css index 28a2957..27b270c 100644 --- a/docs/src/css/custom.css +++ b/docs/src/css/custom.css @@ -165,6 +165,164 @@ 50% { background: transparent; color: #e6edf3; } } +/* ── Benchmark charts ───────────────────────────────────────────── */ + +.bench-results { + display: flex; + flex-direction: column; + gap: 1.25rem; + margin: 1.5rem 0; +} + +.bench-toolbar { + align-items: center; + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.bench-toolbar span { + color: var(--ifm-color-emphasis-700); + font-size: 0.9rem; + font-weight: 700; + margin-right: 0.25rem; +} + +.bench-tab { + background: var(--ifm-background-surface-color); + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: 999px; + color: var(--ifm-color-emphasis-800); + cursor: pointer; + font-size: 0.85rem; + font-weight: 700; + padding: 0.35rem 0.8rem; +} + +.bench-tab-active { + background: var(--ifm-color-primary); + border-color: var(--ifm-color-primary); + color: white; +} + +.bench-card { + background: var(--ifm-background-surface-color); + border: 1px solid var(--ifm-color-emphasis-200); + border-radius: 8px; + padding: 1rem; +} + +.bench-card-heading { + align-items: baseline; + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin: 0 0 1rem; +} + +.bench-card-heading h3 { + margin: 0; +} + +.bench-card-heading span { + color: var(--ifm-color-emphasis-600); + font-size: 0.82rem; + font-weight: 700; +} + +.bench-line-chart { + overflow-x: auto; +} + +.bench-line-chart svg { + display: block; + min-width: 720px; + width: 100%; +} + +.bench-axis { + stroke: var(--ifm-color-emphasis-600); + stroke-width: 1; +} + +.bench-grid { + stroke: var(--ifm-color-emphasis-200); + stroke-width: 1; +} + +.bench-x-tick { + stroke: var(--ifm-color-emphasis-600); + stroke-width: 1; +} + +.bench-axis-label, +.bench-x-label { + fill: var(--ifm-color-emphasis-700); + font-family: var(--ifm-font-family-monospace); + font-size: 12px; +} + +.bench-line { + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2.5; +} + +.bench-point { + stroke: var(--ifm-background-surface-color); + stroke-width: 1.5; +} + +.bench-legend { + display: flex; + flex-wrap: wrap; + gap: 0.5rem 1rem; + margin-top: 0.75rem; +} + +.bench-legend-item { + align-items: center; + color: var(--ifm-color-emphasis-800); + display: inline-flex; + font-size: 0.86rem; + font-weight: 700; + gap: 0.4rem; +} + +.bench-legend-line { + border-top-width: 2.5px; + display: inline-block; + width: 2rem; +} + +.bench-table-wrap { + overflow-x: auto; +} + +.bench-table { + font-size: 0.86rem; + min-width: 760px; +} + +.bench-empty { + background: var(--ifm-alert-background-color); + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: 12px; + padding: 1rem; +} + +.bench-note { + color: var(--ifm-color-emphasis-700); + font-size: 0.9rem; + margin: 0; +} + +@media (max-width: 640px) { + .bench-line-chart svg { + min-width: 680px; + } +} + /* transparent input overlay — keyboard capture only */ .pg-input-hidden { position: absolute; @@ -320,4 +478,3 @@ .pg-env-remove:hover { color: #f85149; } - diff --git a/docs/src/data/benchmarks/results.json b/docs/src/data/benchmarks/results.json new file mode 100644 index 0000000..ce62860 --- /dev/null +++ b/docs/src/data/benchmarks/results.json @@ -0,0 +1,685 @@ +{ + "cases": [ + "simple", + "many_options", + "subcommand" + ], + "compileTime": [ + { + "case": "simple", + "library": "cli20", + "seconds": 1.510937, + "standard": "cxx20" + }, + { + "case": "simple", + "library": "cli11", + "seconds": 3.420628, + "standard": "cxx20" + }, + { + "case": "simple", + "library": "argparse", + "seconds": 1.603756, + "standard": "cxx20" + }, + { + "case": "simple", + "library": "cxxopts", + "seconds": 1.96984, + "standard": "cxx20" + }, + { + "case": "simple", + "library": "boost", + "seconds": 0.830396, + "standard": "cxx20" + }, + { + "case": "many_options", + "library": "cli20", + "seconds": 2.216303, + "standard": "cxx20" + }, + { + "case": "many_options", + "library": "cli11", + "seconds": 3.723533, + "standard": "cxx20" + }, + { + "case": "many_options", + "library": "argparse", + "seconds": 1.800532, + "standard": "cxx20" + }, + { + "case": "many_options", + "library": "cxxopts", + "seconds": 2.134057, + "standard": "cxx20" + }, + { + "case": "many_options", + "library": "boost", + "seconds": 1.095079, + "standard": "cxx20" + }, + { + "case": "subcommand", + "library": "cli20", + "seconds": 1.602702, + "standard": "cxx20" + }, + { + "case": "subcommand", + "library": "cli11", + "seconds": 3.352263, + "standard": "cxx20" + }, + { + "case": "subcommand", + "library": "argparse", + "seconds": 1.683755, + "standard": "cxx20" + }, + { + "case": "subcommand", + "library": "cxxopts", + "seconds": 1.875861, + "standard": "cxx20" + }, + { + "case": "subcommand", + "library": "boost", + "seconds": 0.778103, + "standard": "cxx20" + }, + { + "case": "simple", + "library": "cli20", + "seconds": 1.30442, + "standard": "cxx26" + }, + { + "case": "simple", + "library": "cli11", + "seconds": 3.328135, + "standard": "cxx26" + }, + { + "case": "simple", + "library": "argparse", + "seconds": 1.556079, + "standard": "cxx26" + }, + { + "case": "simple", + "library": "cxxopts", + "seconds": 1.839569, + "standard": "cxx26" + }, + { + "case": "simple", + "library": "boost", + "seconds": 0.73147, + "standard": "cxx26" + }, + { + "case": "many_options", + "library": "cli20", + "seconds": 2.147377, + "standard": "cxx26" + }, + { + "case": "many_options", + "library": "cli11", + "seconds": 3.566478, + "standard": "cxx26" + }, + { + "case": "many_options", + "library": "argparse", + "seconds": 1.702517, + "standard": "cxx26" + }, + { + "case": "many_options", + "library": "cxxopts", + "seconds": 1.961641, + "standard": "cxx26" + }, + { + "case": "many_options", + "library": "boost", + "seconds": 1.042563, + "standard": "cxx26" + }, + { + "case": "subcommand", + "library": "cli20", + "seconds": 1.534191, + "standard": "cxx26" + }, + { + "case": "subcommand", + "library": "cli11", + "seconds": 3.352722, + "standard": "cxx26" + }, + { + "case": "subcommand", + "library": "argparse", + "seconds": 1.588675, + "standard": "cxx26" + }, + { + "case": "subcommand", + "library": "cxxopts", + "seconds": 1.813563, + "standard": "cxx26" + }, + { + "case": "subcommand", + "library": "boost", + "seconds": 0.752213, + "standard": "cxx26" + } + ], + "libraries": [ + "cli20", + "cli11", + "argparse", + "cxxopts", + "boost" + ], + "runtime": [ + { + "allocatedBytes": 0.0, + "allocations": 0.0, + "case": "simple", + "cpuNs": 231.7489955487378, + "library": "cli20", + "standard": "cxx20", + "timeNs": 231.78580580568487 + }, + { + "allocatedBytes": 4248.0, + "allocations": 81.0, + "case": "simple", + "cpuNs": 2897.6993522448342, + "library": "cli11", + "standard": "cxx20", + "timeNs": 2898.723031875759 + }, + { + "allocatedBytes": 3552.0, + "allocations": 34.0, + "case": "simple", + "cpuNs": 1370.807262283304, + "library": "argparse", + "standard": "cxx20", + "timeNs": 1370.871370269089 + }, + { + "allocatedBytes": 21920.0, + "allocations": 330.0, + "case": "simple", + "cpuNs": 9120.030007501857, + "library": "cxxopts", + "standard": "cxx20", + "timeNs": 9119.467355139044 + }, + { + "allocatedBytes": 5120.0, + "allocations": 86.0, + "case": "simple", + "cpuNs": 2759.2039800995058, + "library": "boost", + "standard": "cxx20", + "timeNs": 2760.7627843149858 + }, + { + "allocatedBytes": 0.0, + "allocations": 0.0, + "case": "many_options", + "cpuNs": 3239.5562898276335, + "library": "cli20", + "standard": "cxx20", + "timeNs": 3239.5267887016794 + }, + { + "allocatedBytes": 23344.0, + "allocations": 503.0, + "case": "many_options", + "cpuNs": 17076.92307692294, + "library": "cli11", + "standard": "cxx20", + "timeNs": 17079.146382186293 + }, + { + "allocatedBytes": 13992.0, + "allocations": 98.0, + "case": "many_options", + "cpuNs": 5274.849397590391, + "library": "argparse", + "standard": "cxx20", + "timeNs": 5275.853539617867 + }, + { + "allocatedBytes": 126472.0, + "allocations": 1489.0, + "case": "many_options", + "cpuNs": 41828.40236686452, + "library": "cxxopts", + "standard": "cxx20", + "timeNs": 41843.81068841218 + }, + { + "allocatedBytes": 27632.0, + "allocations": 412.0, + "case": "many_options", + "cpuNs": 20011.4285714286, + "library": "boost", + "standard": "cxx20", + "timeNs": 20017.857147779847 + }, + { + "allocatedBytes": 0.0, + "allocations": 0.0, + "case": "subcommand", + "cpuNs": 193.68964897981823, + "library": "cli20", + "standard": "cxx20", + "timeNs": 193.8485130367405 + }, + { + "allocatedBytes": 7112.0, + "allocations": 107.0, + "case": "subcommand", + "cpuNs": 3748.1309615880587, + "library": "cli11", + "standard": "cxx20", + "timeNs": 3754.1139473407293 + }, + { + "allocatedBytes": 5920.0, + "allocations": 64.0, + "case": "subcommand", + "cpuNs": 2226.506426306124, + "library": "argparse", + "standard": "cxx20", + "timeNs": 2226.3395075520407 + }, + { + "allocatedBytes": 7776.0, + "allocations": 105.0, + "case": "subcommand", + "cpuNs": 2727.5100401606674, + "library": "cxxopts", + "standard": "cxx20", + "timeNs": 2729.593376193315 + }, + { + "allocatedBytes": 1752.0, + "allocations": 36.0, + "case": "subcommand", + "cpuNs": 951.7006802721053, + "library": "boost", + "standard": "cxx20", + "timeNs": 952.2590466274494 + }, + { + "allocatedBytes": 0.0, + "allocations": 0.0, + "case": "simple", + "cpuNs": 225.83251961345562, + "library": "cli20", + "standard": "cxx26", + "timeNs": 225.8262064819953 + }, + { + "allocatedBytes": 4248.0, + "allocations": 81.0, + "case": "simple", + "cpuNs": 2827.43650965921, + "library": "cli11", + "standard": "cxx26", + "timeNs": 2827.29195085394 + }, + { + "allocatedBytes": 3552.0, + "allocations": 34.0, + "case": "simple", + "cpuNs": 1372.4985139686976, + "library": "argparse", + "standard": "cxx26", + "timeNs": 1372.8907255659572 + }, + { + "allocatedBytes": 21920.0, + "allocations": 330.0, + "case": "simple", + "cpuNs": 9009.413468501132, + "library": "cxxopts", + "standard": "cxx26", + "timeNs": 9008.327295537027 + }, + { + "allocatedBytes": 5120.0, + "allocations": 86.0, + "case": "simple", + "cpuNs": 2711.0009910803096, + "library": "boost", + "standard": "cxx26", + "timeNs": 2711.892966319492 + }, + { + "allocatedBytes": 0.0, + "allocations": 0.0, + "case": "many_options", + "cpuNs": 3140.7657657657373, + "library": "cli20", + "standard": "cxx26", + "timeNs": 3140.8502223171495 + }, + { + "allocatedBytes": 23344.0, + "allocations": 503.0, + "case": "many_options", + "cpuNs": 16815.256257449357, + "library": "cli11", + "standard": "cxx26", + "timeNs": 16813.46840478249 + }, + { + "allocatedBytes": 13992.0, + "allocations": 98.0, + "case": "many_options", + "cpuNs": 5131.349782293295, + "library": "argparse", + "standard": "cxx26", + "timeNs": 5131.3650206167995 + }, + { + "allocatedBytes": 126472.0, + "allocations": 1489.0, + "case": "many_options", + "cpuNs": 40408.284023668486, + "library": "cxxopts", + "standard": "cxx26", + "timeNs": 40410.748495549495 + }, + { + "allocatedBytes": 27632.0, + "allocations": 412.0, + "case": "many_options", + "cpuNs": 19246.87933425786, + "library": "boost", + "standard": "cxx26", + "timeNs": 19246.59082557536 + }, + { + "allocatedBytes": 0.0, + "allocations": 0.0, + "case": "subcommand", + "cpuNs": 189.68813008981428, + "library": "cli20", + "standard": "cxx26", + "timeNs": 191.42345980316233 + }, + { + "allocatedBytes": 7112.0, + "allocations": 107.0, + "case": "subcommand", + "cpuNs": 3574.1301059001075, + "library": "cli11", + "standard": "cxx26", + "timeNs": 3577.817696490923 + }, + { + "allocatedBytes": 5920.0, + "allocations": 64.0, + "case": "subcommand", + "cpuNs": 2169.029275808921, + "library": "argparse", + "standard": "cxx26", + "timeNs": 2169.0355943162886 + }, + { + "allocatedBytes": 7776.0, + "allocations": 105.0, + "case": "subcommand", + "cpuNs": 2730.244183810804, + "library": "cxxopts", + "standard": "cxx26", + "timeNs": 2730.1399739359954 + }, + { + "allocatedBytes": 1752.0, + "allocations": 36.0, + "case": "subcommand", + "cpuNs": 927.2561055243561, + "library": "boost", + "standard": "cxx26", + "timeNs": 927.5472771934168 + } + ], + "schemaVersion": 1, + "size": [ + { + "case": "many_options", + "library": "argparse", + "standard": "cxx20", + "strippedBytes": 142624, + "unstrippedBytes": 194928 + }, + { + "case": "many_options", + "library": "boost", + "standard": "cxx20", + "strippedBytes": 111104, + "unstrippedBytes": 141808 + }, + { + "case": "many_options", + "library": "cli11", + "standard": "cxx20", + "strippedBytes": 414080, + "unstrippedBytes": 579008 + }, + { + "case": "many_options", + "library": "cli20", + "standard": "cxx20", + "strippedBytes": 232544, + "unstrippedBytes": 372720 + }, + { + "case": "many_options", + "library": "cxxopts", + "standard": "cxx20", + "strippedBytes": 192352, + "unstrippedBytes": 264608 + }, + { + "case": "simple", + "library": "argparse", + "standard": "cxx20", + "strippedBytes": 123744, + "unstrippedBytes": 162968 + }, + { + "case": "simple", + "library": "boost", + "standard": "cxx20", + "strippedBytes": 39136, + "unstrippedBytes": 46680 + }, + { + "case": "simple", + "library": "cli11", + "standard": "cxx20", + "strippedBytes": 380800, + "unstrippedBytes": 505032 + }, + { + "case": "simple", + "library": "cli20", + "standard": "cxx20", + "strippedBytes": 89424, + "unstrippedBytes": 109976 + }, + { + "case": "simple", + "library": "cxxopts", + "standard": "cxx20", + "strippedBytes": 174624, + "unstrippedBytes": 233688 + }, + { + "case": "subcommand", + "library": "argparse", + "standard": "cxx20", + "strippedBytes": 123760, + "unstrippedBytes": 165584 + }, + { + "case": "subcommand", + "library": "boost", + "standard": "cxx20", + "strippedBytes": 38560, + "unstrippedBytes": 46024 + }, + { + "case": "subcommand", + "library": "cli11", + "standard": "cxx20", + "strippedBytes": 381024, + "unstrippedBytes": 507208 + }, + { + "case": "subcommand", + "library": "cli20", + "standard": "cxx20", + "strippedBytes": 123072, + "unstrippedBytes": 147832 + }, + { + "case": "subcommand", + "library": "cxxopts", + "standard": "cxx20", + "strippedBytes": 174624, + "unstrippedBytes": 232704 + }, + { + "case": "many_options", + "library": "argparse", + "standard": "cxx26", + "strippedBytes": 142624, + "unstrippedBytes": 194928 + }, + { + "case": "many_options", + "library": "boost", + "standard": "cxx26", + "strippedBytes": 111104, + "unstrippedBytes": 141808 + }, + { + "case": "many_options", + "library": "cli11", + "standard": "cxx26", + "strippedBytes": 414080, + "unstrippedBytes": 579008 + }, + { + "case": "many_options", + "library": "cli20", + "standard": "cxx26", + "strippedBytes": 232544, + "unstrippedBytes": 372720 + }, + { + "case": "many_options", + "library": "cxxopts", + "standard": "cxx26", + "strippedBytes": 192352, + "unstrippedBytes": 264608 + }, + { + "case": "simple", + "library": "argparse", + "standard": "cxx26", + "strippedBytes": 123744, + "unstrippedBytes": 162968 + }, + { + "case": "simple", + "library": "boost", + "standard": "cxx26", + "strippedBytes": 39136, + "unstrippedBytes": 46680 + }, + { + "case": "simple", + "library": "cli11", + "standard": "cxx26", + "strippedBytes": 380800, + "unstrippedBytes": 505032 + }, + { + "case": "simple", + "library": "cli20", + "standard": "cxx26", + "strippedBytes": 89424, + "unstrippedBytes": 109976 + }, + { + "case": "simple", + "library": "cxxopts", + "standard": "cxx26", + "strippedBytes": 174624, + "unstrippedBytes": 233688 + }, + { + "case": "subcommand", + "library": "argparse", + "standard": "cxx26", + "strippedBytes": 123760, + "unstrippedBytes": 165584 + }, + { + "case": "subcommand", + "library": "boost", + "standard": "cxx26", + "strippedBytes": 38560, + "unstrippedBytes": 46024 + }, + { + "case": "subcommand", + "library": "cli11", + "standard": "cxx26", + "strippedBytes": 381024, + "unstrippedBytes": 507208 + }, + { + "case": "subcommand", + "library": "cli20", + "standard": "cxx26", + "strippedBytes": 123072, + "unstrippedBytes": 147832 + }, + { + "case": "subcommand", + "library": "cxxopts", + "standard": "cxx26", + "strippedBytes": 174624, + "unstrippedBytes": 232704 + } + ], + "standards": [ + "cxx20", + "cxx26" + ] +} diff --git a/flake.nix b/flake.nix index a531ee0..3a04aac 100644 --- a/flake.nix +++ b/flake.nix @@ -80,7 +80,7 @@ pkgs.clang-tools pkgs.doxygen pkgs.emscripten - pkgs.boost + pkgs.boost.dev pkgs.cxxopts pkgs.cli11 pkgs.argparse @@ -179,6 +179,7 @@ pkgs.cxxopts pkgs.cli11 pkgs.argparse + pkgs.boost.dev google_bench ]; @@ -199,6 +200,9 @@ bash bench/compile_time/scripts/run.sh "$RESULTS_DIR/compile_time/cxx$standard" done python3 bench/scripts/summarize.py --output "$RESULTS_DIR/report.md" + python3 bench/scripts/export-doc-data.py \ + "$RESULTS_DIR" \ + "$RESULTS_DIR/docs/benchmark-results.json" runHook postBuild ''; @@ -244,6 +248,21 @@ --expr '{ baseUrl }: (builtins.getFlake (toString ./.)).legacyPackages.${pkgs.system}.buildDoc { inherit baseUrl; }' ''}/bin/build-cli20-doc"; }; + + apps.update_benchmark = { + type = "app"; + program = "${pkgs.writeShellScriptBin "update-cli20-benchmark-docs" '' + set -euo pipefail + + repo_root="$(pwd)" + out_link="$repo_root/result-bench" + data_dir="$repo_root/docs/src/data/benchmarks" + + ${pkgs.nix}/bin/nix build -L "$repo_root#bench" --out-link "$out_link" + mkdir -p "$data_dir" + cp "$out_link/docs/benchmark-results.json" "$data_dir/results.json" + ''}/bin/update-cli20-benchmark-docs"; + }; }; }; } From 802fb95f96d7718f5c0cbe071e37e3aae2e5a1b5 Mon Sep 17 00:00:00 2001 From: gen740 <54583542+gen740@users.noreply.github.com> Date: Wed, 13 May 2026 01:23:55 +0900 Subject: [PATCH 09/15] Move benchmarks after examples in docs sidebar --- docs/sidebars.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sidebars.ts b/docs/sidebars.ts index 9d92699..cbee72b 100644 --- a/docs/sidebars.ts +++ b/docs/sidebars.ts @@ -6,12 +6,12 @@ const sidebars: SidebarsConfig = { 'tutorial', 'actions', 'relations', - 'benchmarks', { type: 'category', label: 'Examples', items: [{type: 'autogenerated', dirName: 'examples'}], }, + 'benchmarks', ], }; From c1dd64f8148042abce9b3b9907917a5bbfb9c9f1 Mon Sep 17 00:00:00 2001 From: gen740 <54583542+gen740@users.noreply.github.com> Date: Wed, 13 May 2026 01:34:53 +0900 Subject: [PATCH 10/15] Make tokenizer stack capacity configurable --- include/cli/parser.hh | 43 +++++++++++++++++----- tests/CMakeLists.txt | 19 ++++++++++ tests/compile_pass/token_stack_capacity.cc | 7 ++++ 3 files changed, 59 insertions(+), 10 deletions(-) create mode 100644 tests/compile_pass/token_stack_capacity.cc diff --git a/include/cli/parser.hh b/include/cli/parser.hh index e09a62a..6e0db13 100644 --- a/include/cli/parser.hh +++ b/include/cli/parser.hh @@ -1,6 +1,7 @@ #pragma once // #include +#include #include #include #include @@ -19,8 +20,25 @@ #include "cli/error.hh" #include "cli/help.hh" +#ifndef CLI20_TOKEN_STACK_CAPACITY +#define CLI20_TOKEN_STACK_CAPACITY 32 +#endif + +static_assert(CLI20_TOKEN_STACK_CAPACITY > 0, + "CLI20_TOKEN_STACK_CAPACITY must be greater than zero"); + namespace cli { +/** + * @brief Number of tokenizer tokens kept in inline stack storage before + * falling back to heap allocation. + * + * Define `CLI20_TOKEN_STACK_CAPACITY` before including `cli/parser.hh` to + * override the default. Because this value changes header-defined types, use + * one value consistently across all translation units in a program. + */ +inline constexpr std::size_t token_stack_capacity = CLI20_TOKEN_STACK_CAPACITY; + namespace detail { inline auto write_message(FILE* file, std::string_view message) -> void { @@ -29,16 +47,21 @@ inline auto write_message(FILE* file, std::string_view message) -> void { (void)std::fwrite("\n", 1, 1, file); } -inline auto value_count_message(int required, int provided) -> std::string { - return "option requires at least " + std::to_string(required) + +inline auto value_count_message(int required_count, int provided) + -> std::string { + return "option requires at least " + std::to_string(required_count) + " value(s), but " + std::to_string(provided) + " provided"; } -inline auto inline_value_count_message(int required) -> std::string { - return "option requires at least " + std::to_string(required) + +inline auto inline_value_count_message(int required_count) -> std::string { + return "option requires at least " + std::to_string(required_count) + " value(s), but inline separator provides only 1"; } +inline auto token_position(std::size_t position) -> std::uint16_t { + return static_cast(position); +} + } // namespace detail #ifdef _WIN32 @@ -212,7 +235,7 @@ struct TokenizeResult { } }; - SmallVector tokens; + SmallVector tokens; ParseError error{}; /** @brief Returns `true` when tokenization succeeded. */ @@ -411,11 +434,11 @@ inline auto tokenize(std::span args, return cfg.option_prefixes()[p & kPrefixIndexMask]; }; - const auto push = [&](TokenType type, std::string_view text, std::uint16_t pos, + const auto push = [&](TokenType type, std::string_view text, std::size_t pos, std::uint8_t prefix = kNoPrefix) -> void { result.tokens.push_back({ .text = text, - .position = pos, + .position = detail::token_position(pos), .prefix = prefix, .type = type, }); @@ -470,11 +493,11 @@ inline auto tokenize(std::span args, const std::string_view char_bare = tok.substr(prefix_sv.size() + k, 1); char_full.back() = body[k]; - const auto nargs_opt = option_nargs(char_full); - if (!nargs_opt) { + const auto char_nargs_opt = option_nargs(char_full); + if (!char_nargs_opt) { return result.fail(ErrorCode::unknown_option, i, char_full); } - const Nargs nargs = *nargs_opt; + const Nargs nargs = *char_nargs_opt; const bool is_flag = (nargs.min == 0 && nargs.max == 0); push(TokenType::option, char_bare, i, prefix); if (!is_flag) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7cafc96..761c61a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -60,3 +60,22 @@ function(add_compile_fail_test name source) message(STATUS "Compile-fail test passed: ${name}") endif() endfunction() + +function(add_compile_pass_test name source) + try_compile( + COMPILE_RESULT + ${CMAKE_BINARY_DIR}/try_compile/${name} + ${CMAKE_CURRENT_SOURCE_DIR}/compile_pass/${source} + CMAKE_FLAGS "-DINCLUDE_DIRECTORIES=${PROJECT_SOURCE_DIR}/include" + CXX_STANDARD 20 + OUTPUT_VARIABLE COMPILE_OUTPUT + ) + if(NOT COMPILE_RESULT) + message(FATAL_ERROR + "Compile-pass test '${name}' failed.\n${COMPILE_OUTPUT}") + else() + message(STATUS "Compile-pass test passed: ${name}") + endif() +endfunction() + +add_compile_pass_test(token_stack_capacity token_stack_capacity.cc) diff --git a/tests/compile_pass/token_stack_capacity.cc b/tests/compile_pass/token_stack_capacity.cc new file mode 100644 index 0000000..7ca848d --- /dev/null +++ b/tests/compile_pass/token_stack_capacity.cc @@ -0,0 +1,7 @@ +#define CLI20_TOKEN_STACK_CAPACITY 4 + +#include "cli/parser.hh" + +static_assert(cli::token_stack_capacity == 4); + +auto main() -> int { return 0; } From 0b7e17bdb00a92d0134a503f6ce90aea353b27cf Mon Sep 17 00:00:00 2001 From: gen740 <54583542+gen740@users.noreply.github.com> Date: Wed, 13 May 2026 01:41:25 +0900 Subject: [PATCH 11/15] ci: update actions/checkout to v6 --- .github/workflows/bump-version.yml | 2 +- .github/workflows/ci.yml | 4 ++-- .github/workflows/deploy-nightly-docs.yml | 2 +- .github/workflows/docs-preview.yml | 2 +- .github/workflows/release-docs.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/windows-unicode.yml | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml index 04fccf5..56ae26b 100644 --- a/.github/workflows/bump-version.yml +++ b/.github/workflows/bump-version.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Compute next patch version id: next diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7416478..52663c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: name: Format check runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 @@ -52,7 +52,7 @@ jobs: - {os: windows-latest, compiler: msvc, cc: "", cxx: ""} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Ninja (Linux) if: runner.os == 'Linux' diff --git a/.github/workflows/deploy-nightly-docs.yml b/.github/workflows/deploy-nightly-docs.yml index 4b054ab..08cf337 100644 --- a/.github/workflows/deploy-nightly-docs.yml +++ b/.github/workflows/deploy-nightly-docs.yml @@ -22,7 +22,7 @@ jobs: contents: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 diff --git a/.github/workflows/docs-preview.yml b/.github/workflows/docs-preview.yml index 2dc7c2f..bacba88 100644 --- a/.github/workflows/docs-preview.yml +++ b/.github/workflows/docs-preview.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: cachix/install-nix-action@v27 with: diff --git a/.github/workflows/release-docs.yml b/.github/workflows/release-docs.yml index c614443..78d3b77 100644 --- a/.github/workflows/release-docs.yml +++ b/.github/workflows/release-docs.yml @@ -17,7 +17,7 @@ jobs: contents: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f15a194..f86f3c9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install clang run: sudo apt-get install -y clang diff --git a/.github/workflows/windows-unicode.yml b/.github/workflows/windows-unicode.yml index 1149191..8b6a8df 100644 --- a/.github/workflows/windows-unicode.yml +++ b/.github/workflows/windows-unicode.yml @@ -13,7 +13,7 @@ jobs: runs-on: windows-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: lukka/get-cmake@latest From 54b8bfe1695807ece6f92aa815a9ea09463a2d9c Mon Sep 17 00:00:00 2001 From: gen740 <54583542+gen740@users.noreply.github.com> Date: Wed, 13 May 2026 02:12:52 +0900 Subject: [PATCH 12/15] Address PR tokenizer review feedback --- include/cli/action.hh | 10 +- include/cli/argument.hh | 6 +- include/cli/parser.hh | 206 +++++++++++++++++++++++++--------------- tests/test_tokenize.cc | 6 +- 4 files changed, 147 insertions(+), 81 deletions(-) diff --git a/include/cli/action.hh b/include/cli/action.hh index 83aa8c3..9f355e9 100644 --- a/include/cli/action.hh +++ b/include/cli/action.hh @@ -297,8 +297,14 @@ template [[nodiscard]] auto to_chars_string(T value) -> std::string { std::array buffer{}; - const auto [ptr, ec] = - std::to_chars(buffer.data(), buffer.data() + buffer.size(), value); + const auto [ptr, ec] = [&] { + if constexpr (std::floating_point) { + return std::to_chars(buffer.data(), buffer.data() + buffer.size(), value, + std::chars_format::general); + } else { + return std::to_chars(buffer.data(), buffer.data() + buffer.size(), value); + } + }(); if (ec != std::errc{}) return ""; return std::string(buffer.data(), ptr); } diff --git a/include/cli/argument.hh b/include/cli/argument.hh index d129ded..8dbf9db 100644 --- a/include/cli/argument.hh +++ b/include/cli/argument.hh @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -378,7 +379,10 @@ struct PositionalImpl : public PositionalTag { struct Description : DescriptionTag { std::string_view text{}; constexpr Description() = default; - constexpr Description(std::string_view s) : text(s) {} + template + // NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays, modernize-avoid-c-arrays) + constexpr Description(const char (&s)[N]) : text(s, N - 1) {} + constexpr Description(std::string_view) = delete; constexpr operator std::string_view() const { return text; } }; diff --git a/include/cli/parser.hh b/include/cli/parser.hh index 6e0db13..05dfafa 100644 --- a/include/cli/parser.hh +++ b/include/cli/parser.hh @@ -2,11 +2,16 @@ // #include #include +#include #include #include +#include #include #include +#include #include +#include +#include #include #ifdef _WIN32 @@ -58,10 +63,6 @@ inline auto inline_value_count_message(int required_count) -> std::string { " value(s), but inline separator provides only 1"; } -inline auto token_position(std::size_t position) -> std::uint16_t { - return static_cast(position); -} - } // namespace detail #ifdef _WIN32 @@ -171,11 +172,11 @@ static constexpr auto is_short_prefix(std::uint8_t p) -> bool { * @brief A single token produced by `tokenize()`. */ struct Token { - std::string_view text; ///< For option tokens: the bare name without prefix - ///< (e.g. `"verbose"` for `--verbose`, `"v"` for - ///< `-v`). For value/positional/command tokens: the - ///< raw text from the args span. - std::uint16_t position; ///< Zero-based index into the original `args` span. + std::string_view text; ///< For option tokens: the bare name without prefix + ///< (e.g. `"verbose"` for `--verbose`, `"v"` for + ///< `-v`). For value/positional/command tokens: the + ///< raw text from the args span. + std::size_t position; ///< Zero-based index into the original `args` span. std::uint8_t prefix; // 0xff = none, high bit = short prefix, low 7bit = long // prefix index TokenType type; ///< Classification of this token. @@ -288,9 +289,9 @@ struct TokenizeResult { * `=` as the inline value separator and `--` as the end-of-options marker. */ struct TokenizerConfig { - private: static constexpr std::size_t max_option_prefixes = 4; + private: std::array option_prefixes_{"--"}; std::uint8_t option_prefix_count_{1}; @@ -319,35 +320,36 @@ struct TokenizerConfig { return inline_value_separator_; } - constexpr auto set_option_prefixes( - std::initializer_list prefixes) -> void { + template + constexpr auto set_option_prefixes() -> void { + static_assert(sizeof...(Prefixes) > 0, + "TokenizerConfig needs at least one option prefix"); + static_assert(sizeof...(Prefixes) <= max_option_prefixes, + "Too many option prefixes for TokenizerConfig"); option_prefix_count_ = 0; - - for (auto p : prefixes) { - if (option_prefix_count_ >= max_option_prefixes) break; - option_prefixes_[option_prefix_count_++] = p; - } + ((option_prefixes_[option_prefix_count_++] = Prefixes.view()), ...); } - [[nodiscard]] constexpr auto with_option_prefixes( - std::initializer_list prefixes) const - -> TokenizerConfig { + template + [[nodiscard]] constexpr auto with_option_prefixes() const -> TokenizerConfig { auto cfg = *this; - cfg.set_option_prefixes(prefixes); + cfg.template set_option_prefixes(); return cfg; } - [[nodiscard]] constexpr auto with_short_option_prefix( - std::string_view prefix) const -> TokenizerConfig { + template + [[nodiscard]] constexpr auto with_short_option_prefix() const + -> TokenizerConfig { auto cfg = *this; - cfg.short_option_prefix_ = prefix; + cfg.short_option_prefix_ = Prefix.view(); return cfg; } - [[nodiscard]] constexpr auto with_end_of_options_separator( - std::string_view separator) const -> TokenizerConfig { + template + [[nodiscard]] constexpr auto with_end_of_options_separator() const + -> TokenizerConfig { auto cfg = *this; - cfg.end_of_options_separator_ = separator; + cfg.end_of_options_separator_ = Separator.view(); return cfg; } @@ -438,7 +440,7 @@ inline auto tokenize(std::span args, std::uint8_t prefix = kNoPrefix) -> void { result.tokens.push_back({ .text = text, - .position = detail::token_position(pos), + .position = pos, .prefix = prefix, .type = type, }); @@ -478,8 +480,7 @@ inline auto tokenize(std::span args, const bool has_inline_val = (eq_pos != std::string_view::npos); const std::string_view opt_sv = has_inline_val ? tok.substr(0, eq_pos) : tok; - const std::string opt_name{opt_sv}; - const auto nargs_opt = option_nargs(opt_name); + const auto nargs_opt = option_nargs(opt_sv); if (!nargs_opt) { // Short cluster (-xvf) or attached value (-ofile): only when the token // uses the short prefix and carries more than one character after it. @@ -510,13 +511,14 @@ inline auto tokenize(std::span args, while (i < args.size()) { const std::string_view next = args[i]; if (next == cfg.end_of_options_separator()) break; - if (is_command(std::string(next))) break; + if (is_command(next)) break; if (nargs.max != -1 && count >= nargs.max) break; if (has_prefix(find_prefix(next))) { const auto next_sep = next.find(sep); - const std::string next_opt{next_sep != std::string_view::npos - ? next.substr(0, next_sep) - : next}; + const std::string_view next_opt = + next_sep != std::string_view::npos + ? next.substr(0, next_sep) + : next; if (option_nargs(next_opt).has_value()) break; if (count >= 1 || nargs.min == 0) break; } @@ -536,13 +538,14 @@ inline auto tokenize(std::span args, while (i < args.size()) { const std::string_view next = args[i]; if (next == cfg.end_of_options_separator()) break; - if (is_command(std::string(next))) break; + if (is_command(next)) break; if (nargs.max != -1 && count >= nargs.max) break; if (has_prefix(find_prefix(next))) { const auto next_sep = next.find(sep); - const std::string next_opt{next_sep != std::string_view::npos - ? next.substr(0, next_sep) - : next}; + const std::string_view next_opt = + next_sep != std::string_view::npos + ? next.substr(0, next_sep) + : next; if (option_nargs(next_opt).has_value()) break; if (count >= 1 || nargs.min == 0) break; } @@ -563,7 +566,7 @@ inline auto tokenize(std::span args, if (!advanced_i) ++i; continue; } - return result.fail(ErrorCode::unknown_option, i, opt_name); + return result.fail(ErrorCode::unknown_option, i, std::string(opt_sv)); } const Nargs& nargs = *nargs_opt; @@ -576,11 +579,11 @@ inline auto tokenize(std::span args, if (has_inline_val) { if (is_flag) { - return result.fail(ErrorCode::invalid_value, i, opt_name, + return result.fail(ErrorCode::invalid_value, i, std::string(opt_sv), "flag does not accept a value"); } if (nargs.min > 1) { - return result.fail(ErrorCode::missing_value, i, opt_name, + return result.fail(ErrorCode::missing_value, i, std::string(opt_sv), detail::inline_value_count_message(nargs.min)); } push(TokenType::value, tok.substr(eq_pos + 1), i); @@ -598,14 +601,14 @@ inline auto tokenize(std::span args, const std::string_view next = args[i]; if (next == cfg.end_of_options_separator()) break; - if (is_command(std::string(next))) break; + if (is_command(next)) break; if (nargs.max != -1 && count >= nargs.max) break; if (has_prefix(find_prefix(next))) { const auto next_sep = next.find(sep); - const std::string next_opt{next_sep != std::string_view::npos - ? next.substr(0, next_sep) - : next}; + const std::string_view next_opt = next_sep != std::string_view::npos + ? next.substr(0, next_sep) + : next; if (option_nargs(next_opt).has_value()) break; // Unknown option-looking: consume only as first value when min > 0 @@ -618,7 +621,7 @@ inline auto tokenize(std::span args, } if (count < nargs.min) { - return result.fail(ErrorCode::missing_value, i, opt_name, + return result.fail(ErrorCode::missing_value, i, std::string(opt_sv), detail::value_count_message(nargs.min, count)); } continue; @@ -868,6 +871,9 @@ struct Parser { TokenizerConfig cfg_; std::string program_name_ = "program"; + static constexpr std::size_t field_count = std::tuple_size_v< + std::remove_reference_t()))>>; + auto parse_body(ParseResult& result, std::span args, std::size_t first_index) -> void { auto lookup = get_option_spec_map(result.value); @@ -1033,48 +1039,98 @@ struct Parser { std::tuple_size_v>>{}); } - // Build the option spec-map and command-name set from the field types of val. + // Build a flat lookup table once per parse so tokenization does not reflect + // over every field for every argv token. struct Lookup { - const Parser* self; - const T* val; + struct OptionEntry { + std::string_view prefix; + std::string_view name; + char short_name{}; + Nargs nargs{}; + bool is_short{}; + }; + + struct CommandEntry { + std::string_view name; + }; + + std::array + options{}; + std::array commands{}; + std::size_t option_count{}; + std::size_t command_count{}; + + auto add_long_option(std::string_view prefix, std::string_view name, + Nargs nargs) -> void { + options[option_count++] = OptionEntry{ + .prefix = prefix, + .name = name, + .short_name = '\0', + .nargs = nargs, + .is_short = false, + }; + } + + auto add_short_option(std::string_view prefix, char short_name, Nargs nargs) + -> void { + options[option_count++] = OptionEntry{ + .prefix = prefix, + .name = {}, + .short_name = short_name, + .nargs = nargs, + .is_short = true, + }; + } + + auto add_command(std::string_view name) -> void { + commands[command_count++] = CommandEntry{.name = name}; + } [[nodiscard]] auto option_nargs(std::string_view opt) const -> std::optional { - std::optional found; - self->for_each_field(*val, [&](const auto& f) -> auto { - using F = std::remove_cvref_t; - if constexpr (std::derived_from) { - for (const auto& p : self->cfg_.option_prefixes()) { - if (opt.starts_with(p) && opt.substr(p.size()) == F::name.view()) { - found = F::nargs; - } - } - if constexpr (F::short_name != '\0') { - const auto& sp = self->cfg_.short_option_prefix(); - if (opt.starts_with(sp) && opt.size() == sp.size() + 1 && - opt.back() == F::short_name) { - found = F::nargs; - } + for (std::size_t i = 0; i < option_count; ++i) { + const auto& entry = options[i]; + if (!opt.starts_with(entry.prefix)) { + continue; + } + const auto rest = opt.substr(entry.prefix.size()); + if (entry.is_short) { + if (rest.size() == 1 && rest.front() == entry.short_name) { + return entry.nargs; } + } else if (rest == entry.name) { + return entry.nargs; } - }); - return found; + } + return std::nullopt; } [[nodiscard]] auto is_command(std::string_view name) const -> bool { - bool found = false; - self->for_each_field(*val, [&](const auto& f) -> auto { - using F = std::remove_cvref_t; - if constexpr (std::derived_from) { - if (F::command_name() == name) found = true; - } - }); - return found; + for (std::size_t i = 0; i < command_count; ++i) { + if (commands[i].name == name) return true; + } + return false; } }; auto get_option_spec_map(const T& val) const -> Lookup { - return Lookup{.self = this, .val = &val}; + Lookup lookup{}; + for_each_field(val, [&](const auto& f) -> auto { + using F = std::remove_cvref_t; + if constexpr (std::derived_from) { + for (const auto& p : cfg_.option_prefixes()) { + lookup.add_long_option(p, F::name.view(), F::nargs); + } + if constexpr (F::short_name != '\0') { + lookup.add_short_option(cfg_.short_option_prefix(), F::short_name, + F::nargs); + } + } else if constexpr (std::derived_from) { + lookup.add_command(F::command_name()); + } + }); + return lookup; } // Find the option field matching (prefix, bare), notify it, and invoke its diff --git a/tests/test_tokenize.cc b/tests/test_tokenize.cc index 6d930c4..4d3ebd8 100644 --- a/tests/test_tokenize.cc +++ b/tests/test_tokenize.cc @@ -472,7 +472,7 @@ static auto tokenize_cfg(std::initializer_list args, TEST(TokenizeMultiPrefix, AltPrefixRecognised) { // "+verbose" recognised as option when "+" is in option_prefixes - auto cfg = cli::TokenizerConfig{}.with_option_prefixes({"--", "+"}); + auto cfg = cli::TokenizerConfig{}.with_option_prefixes<"--", "+">(); auto r = tokenize_cfg({"+verbose"}, {{"+verbose", kFlag}}, cfg); ASSERT_TRUE(r.has_value()); ASSERT_EQ(r.tokens.size(), 1u); @@ -483,7 +483,7 @@ TEST(TokenizeMultiPrefix, AltPrefixRecognised) { TEST(TokenizeMultiPrefix, MatchedPrefixDistinguishesPrefixes) { // "--foo" and "+foo" are different options; matched_prefix tells them apart - auto cfg = cli::TokenizerConfig{}.with_option_prefixes({"--", "+"}); + auto cfg = cli::TokenizerConfig{}.with_option_prefixes<"--", "+">(); SpecMap spec{{"--foo", kFlag}, {"+foo", kFlag}}; auto r = tokenize_cfg({"--foo", "+foo"}, spec, cfg); ASSERT_TRUE(r.has_value()); @@ -502,7 +502,7 @@ TEST(TokenizeMultiPrefix, DefaultPrefixStillWorks) { } TEST(TokenizeMultiPrefix, UnknownAltPrefixTokenIsError) { - auto cfg = cli::TokenizerConfig{}.with_option_prefixes({"--", "+"}); + auto cfg = cli::TokenizerConfig{}.with_option_prefixes<"--", "+">(); auto r = tokenize_cfg({"+unknown"}, {{"--foo", kFlag}}, cfg); ASSERT_FALSE(r.has_value()); EXPECT_EQ(r.error.code, ErrorCode::unknown_option); From 01c693e01247b6959f8578bd1f6b44b68c0da396 Mon Sep 17 00:00:00 2001 From: gen740 <54583542+gen740@users.noreply.github.com> Date: Wed, 13 May 2026 02:20:08 +0900 Subject: [PATCH 13/15] docs(bench): update benchmark results --- docs/src/data/benchmarks/results.json | 196 +++++++++++++------------- 1 file changed, 98 insertions(+), 98 deletions(-) diff --git a/docs/src/data/benchmarks/results.json b/docs/src/data/benchmarks/results.json index ce62860..a5f740e 100644 --- a/docs/src/data/benchmarks/results.json +++ b/docs/src/data/benchmarks/results.json @@ -8,181 +8,181 @@ { "case": "simple", "library": "cli20", - "seconds": 1.510937, + "seconds": 1.568617, "standard": "cxx20" }, { "case": "simple", "library": "cli11", - "seconds": 3.420628, + "seconds": 3.743366, "standard": "cxx20" }, { "case": "simple", "library": "argparse", - "seconds": 1.603756, + "seconds": 1.781824, "standard": "cxx20" }, { "case": "simple", "library": "cxxopts", - "seconds": 1.96984, + "seconds": 2.084148, "standard": "cxx20" }, { "case": "simple", "library": "boost", - "seconds": 0.830396, + "seconds": 0.88167, "standard": "cxx20" }, { "case": "many_options", "library": "cli20", - "seconds": 2.216303, + "seconds": 2.155856, "standard": "cxx20" }, { "case": "many_options", "library": "cli11", - "seconds": 3.723533, + "seconds": 3.508925, "standard": "cxx20" }, { "case": "many_options", "library": "argparse", - "seconds": 1.800532, + "seconds": 1.82392, "standard": "cxx20" }, { "case": "many_options", "library": "cxxopts", - "seconds": 2.134057, + "seconds": 2.189928, "standard": "cxx20" }, { "case": "many_options", "library": "boost", - "seconds": 1.095079, + "seconds": 1.138985, "standard": "cxx20" }, { "case": "subcommand", "library": "cli20", - "seconds": 1.602702, + "seconds": 1.587791, "standard": "cxx20" }, { "case": "subcommand", "library": "cli11", - "seconds": 3.352263, + "seconds": 3.255969, "standard": "cxx20" }, { "case": "subcommand", "library": "argparse", - "seconds": 1.683755, + "seconds": 1.582255, "standard": "cxx20" }, { "case": "subcommand", "library": "cxxopts", - "seconds": 1.875861, + "seconds": 1.802998, "standard": "cxx20" }, { "case": "subcommand", "library": "boost", - "seconds": 0.778103, + "seconds": 0.754448, "standard": "cxx20" }, { "case": "simple", "library": "cli20", - "seconds": 1.30442, + "seconds": 1.236948, "standard": "cxx26" }, { "case": "simple", "library": "cli11", - "seconds": 3.328135, + "seconds": 3.520713, "standard": "cxx26" }, { "case": "simple", "library": "argparse", - "seconds": 1.556079, + "seconds": 1.70554, "standard": "cxx26" }, { "case": "simple", "library": "cxxopts", - "seconds": 1.839569, + "seconds": 1.861667, "standard": "cxx26" }, { "case": "simple", "library": "boost", - "seconds": 0.73147, + "seconds": 0.788324, "standard": "cxx26" }, { "case": "many_options", "library": "cli20", - "seconds": 2.147377, + "seconds": 2.258437, "standard": "cxx26" }, { "case": "many_options", "library": "cli11", - "seconds": 3.566478, + "seconds": 3.7834, "standard": "cxx26" }, { "case": "many_options", "library": "argparse", - "seconds": 1.702517, + "seconds": 1.853235, "standard": "cxx26" }, { "case": "many_options", "library": "cxxopts", - "seconds": 1.961641, + "seconds": 2.192126, "standard": "cxx26" }, { "case": "many_options", "library": "boost", - "seconds": 1.042563, + "seconds": 1.164148, "standard": "cxx26" }, { "case": "subcommand", "library": "cli20", - "seconds": 1.534191, + "seconds": 1.71208, "standard": "cxx26" }, { "case": "subcommand", "library": "cli11", - "seconds": 3.352722, + "seconds": 3.582609, "standard": "cxx26" }, { "case": "subcommand", "library": "argparse", - "seconds": 1.588675, + "seconds": 1.571239, "standard": "cxx26" }, { "case": "subcommand", "library": "cxxopts", - "seconds": 1.813563, + "seconds": 1.836612, "standard": "cxx26" }, { "case": "subcommand", "library": "boost", - "seconds": 0.752213, + "seconds": 0.753532, "standard": "cxx26" } ], @@ -198,271 +198,271 @@ "allocatedBytes": 0.0, "allocations": 0.0, "case": "simple", - "cpuNs": 231.7489955487378, + "cpuNs": 253.32093755028515, "library": "cli20", "standard": "cxx20", - "timeNs": 231.78580580568487 + "timeNs": 253.51536655200294 }, { "allocatedBytes": 4248.0, "allocations": 81.0, "case": "simple", - "cpuNs": 2897.6993522448342, + "cpuNs": 3007.5960679178525, "library": "cli11", "standard": "cxx20", - "timeNs": 2898.723031875759 + "timeNs": 3008.3965538843627 }, { "allocatedBytes": 3552.0, "allocations": 34.0, "case": "simple", - "cpuNs": 1370.807262283304, + "cpuNs": 1417.6598611255279, "library": "argparse", "standard": "cxx20", - "timeNs": 1370.871370269089 + "timeNs": 1419.0460132111236 }, { "allocatedBytes": 21920.0, "allocations": 330.0, "case": "simple", - "cpuNs": 9120.030007501857, + "cpuNs": 9319.595083152675, "library": "cxxopts", "standard": "cxx20", - "timeNs": 9119.467355139044 + "timeNs": 9319.775833614272 }, { "allocatedBytes": 5120.0, "allocations": 86.0, "case": "simple", - "cpuNs": 2759.2039800995058, + "cpuNs": 2787.335722819585, "library": "boost", "standard": "cxx20", - "timeNs": 2760.7627843149858 + "timeNs": 2788.281564419768 }, { "allocatedBytes": 0.0, "allocations": 0.0, "case": "many_options", - "cpuNs": 3239.5562898276335, + "cpuNs": 2731.753916698495, "library": "cli20", "standard": "cxx20", - "timeNs": 3239.5267887016794 + "timeNs": 2732.860339429325 }, { "allocatedBytes": 23344.0, "allocations": 503.0, "case": "many_options", - "cpuNs": 17076.92307692294, + "cpuNs": 17230.110159118598, "library": "cli11", "standard": "cxx20", - "timeNs": 17079.146382186293 + "timeNs": 17231.487122739378 }, { "allocatedBytes": 13992.0, "allocations": 98.0, "case": "many_options", - "cpuNs": 5274.849397590391, + "cpuNs": 5355.455568053927, "library": "argparse", "standard": "cxx20", - "timeNs": 5275.853539617867 + "timeNs": 5357.267719529008 }, { "allocatedBytes": 126472.0, "allocations": 1489.0, "case": "many_options", - "cpuNs": 41828.40236686452, + "cpuNs": 42796.40718562883, "library": "cxxopts", "standard": "cxx20", - "timeNs": 41843.81068841218 + "timeNs": 42829.34128649778 }, { "allocatedBytes": 27632.0, "allocations": 412.0, "case": "many_options", - "cpuNs": 20011.4285714286, + "cpuNs": 20211.347517730752, "library": "boost", "standard": "cxx20", - "timeNs": 20017.857147779847 + "timeNs": 20222.458162787534 }, { "allocatedBytes": 0.0, "allocations": 0.0, "case": "subcommand", - "cpuNs": 193.68964897981823, + "cpuNs": 200.9111420893017, "library": "cli20", "standard": "cxx20", - "timeNs": 193.8485130367405 + "timeNs": 201.0356986937043 }, { "allocatedBytes": 7112.0, "allocations": 107.0, "case": "subcommand", - "cpuNs": 3748.1309615880587, + "cpuNs": 3680.5483704087032, "library": "cli11", "standard": "cxx20", - "timeNs": 3754.1139473407293 + "timeNs": 3681.486037420567 }, { "allocatedBytes": 5920.0, "allocations": 64.0, "case": "subcommand", - "cpuNs": 2226.506426306124, + "cpuNs": 2283.2128514056276, "library": "argparse", "standard": "cxx20", - "timeNs": 2226.3395075520407 + "timeNs": 2284.819278005795 }, { "allocatedBytes": 7776.0, "allocations": 105.0, "case": "subcommand", - "cpuNs": 2727.5100401606674, + "cpuNs": 2853.648675965263, "library": "cxxopts", "standard": "cxx20", - "timeNs": 2729.593376193315 + "timeNs": 2855.434201899498 }, { "allocatedBytes": 1752.0, "allocations": 36.0, "case": "subcommand", - "cpuNs": 951.7006802721053, + "cpuNs": 959.8003507351925, "library": "boost", "standard": "cxx20", - "timeNs": 952.2590466274494 + "timeNs": 960.1038700079201 }, { "allocatedBytes": 0.0, "allocations": 0.0, "case": "simple", - "cpuNs": 225.83251961345562, + "cpuNs": 243.00800742717982, "library": "cli20", "standard": "cxx26", - "timeNs": 225.8262064819953 + "timeNs": 243.42039846220192 }, { "allocatedBytes": 4248.0, "allocations": 81.0, "case": "simple", - "cpuNs": 2827.43650965921, + "cpuNs": 2882.654197283434, "library": "cli11", "standard": "cxx26", - "timeNs": 2827.29195085394 + "timeNs": 2883.8975749249594 }, { "allocatedBytes": 3552.0, "allocations": 34.0, "case": "simple", - "cpuNs": 1372.4985139686976, + "cpuNs": 1361.5546851461324, "library": "argparse", "standard": "cxx26", - "timeNs": 1372.8907255659572 + "timeNs": 1361.5212405716484 }, { "allocatedBytes": 21920.0, "allocations": 330.0, "case": "simple", - "cpuNs": 9009.413468501132, + "cpuNs": 8794.838709677477, "library": "cxxopts", "standard": "cxx26", - "timeNs": 9008.327295537027 + "timeNs": 8798.118064840955 }, { "allocatedBytes": 5120.0, "allocations": 86.0, "case": "simple", - "cpuNs": 2711.0009910803096, + "cpuNs": 2659.7878495660766, "library": "boost", "standard": "cxx26", - "timeNs": 2711.892966319492 + "timeNs": 2659.6995174306426 }, { "allocatedBytes": 0.0, "allocations": 0.0, "case": "many_options", - "cpuNs": 3140.7657657657373, + "cpuNs": 2839.222941720603, "library": "cli20", "standard": "cxx26", - "timeNs": 3140.8502223171495 + "timeNs": 2858.84976808316 }, { "allocatedBytes": 23344.0, "allocations": 503.0, "case": "many_options", - "cpuNs": 16815.256257449357, + "cpuNs": 16879.898862199785, "library": "cli11", "standard": "cxx26", - "timeNs": 16813.46840478249 + "timeNs": 16883.533467407964 }, { "allocatedBytes": 13992.0, "allocations": 98.0, "case": "many_options", - "cpuNs": 5131.349782293295, + "cpuNs": 5247.54634678303, "library": "argparse", "standard": "cxx26", - "timeNs": 5131.3650206167995 + "timeNs": 5249.954558304985 }, { "allocatedBytes": 126472.0, "allocations": 1489.0, "case": "many_options", - "cpuNs": 40408.284023668486, + "cpuNs": 40789.62536022991, "library": "cxxopts", "standard": "cxx26", - "timeNs": 40410.748495549495 + "timeNs": 40790.4668668183 }, { "allocatedBytes": 27632.0, "allocations": 412.0, "case": "many_options", - "cpuNs": 19246.87933425786, + "cpuNs": 19525.64102564122, "library": "boost", "standard": "cxx26", - "timeNs": 19246.59082557536 + "timeNs": 19525.878922085136 }, { "allocatedBytes": 0.0, "allocations": 0.0, "case": "subcommand", - "cpuNs": 189.68813008981428, + "cpuNs": 197.47375418963608, "library": "cli20", "standard": "cxx26", - "timeNs": 191.42345980316233 + "timeNs": 197.7622918708773 }, { "allocatedBytes": 7112.0, "allocations": 107.0, "case": "subcommand", - "cpuNs": 3574.1301059001075, + "cpuNs": 3575.741801145244, "library": "cli11", "standard": "cxx26", - "timeNs": 3577.817696490923 + "timeNs": 3576.6637176255863 }, { "allocatedBytes": 5920.0, "allocations": 64.0, "case": "subcommand", - "cpuNs": 2169.029275808921, + "cpuNs": 2189.168456581753, "library": "argparse", "standard": "cxx26", - "timeNs": 2169.0355943162886 + "timeNs": 2190.363759681612 }, { "allocatedBytes": 7776.0, "allocations": 105.0, "case": "subcommand", - "cpuNs": 2730.244183810804, + "cpuNs": 2692.5484060238773, "library": "cxxopts", "standard": "cxx26", - "timeNs": 2730.1399739359954 + "timeNs": 2692.548405384886 }, { "allocatedBytes": 1752.0, "allocations": 36.0, "case": "subcommand", - "cpuNs": 927.2561055243561, + "cpuNs": 944.422546314541, "library": "boost", "standard": "cxx26", - "timeNs": 927.5472771934168 + "timeNs": 944.6744185754538 } ], "schemaVersion": 1, @@ -492,8 +492,8 @@ "case": "many_options", "library": "cli20", "standard": "cxx20", - "strippedBytes": 232544, - "unstrippedBytes": 372720 + "strippedBytes": 216064, + "unstrippedBytes": 356224 }, { "case": "many_options", @@ -528,7 +528,7 @@ "library": "cli20", "standard": "cxx20", "strippedBytes": 89424, - "unstrippedBytes": 109976 + "unstrippedBytes": 109384 }, { "case": "simple", @@ -563,7 +563,7 @@ "library": "cli20", "standard": "cxx20", "strippedBytes": 123072, - "unstrippedBytes": 147832 + "unstrippedBytes": 147720 }, { "case": "subcommand", @@ -597,8 +597,8 @@ "case": "many_options", "library": "cli20", "standard": "cxx26", - "strippedBytes": 232544, - "unstrippedBytes": 372720 + "strippedBytes": 216064, + "unstrippedBytes": 356224 }, { "case": "many_options", @@ -633,7 +633,7 @@ "library": "cli20", "standard": "cxx26", "strippedBytes": 89424, - "unstrippedBytes": 109976 + "unstrippedBytes": 109384 }, { "case": "simple", @@ -668,7 +668,7 @@ "library": "cli20", "standard": "cxx26", "strippedBytes": 123072, - "unstrippedBytes": 147832 + "unstrippedBytes": 147720 }, { "case": "subcommand", From 2a50535b31d9113f256420527480d4700c49e8db Mon Sep 17 00:00:00 2001 From: gen740 <54583542+gen740@users.noreply.github.com> Date: Wed, 13 May 2026 02:27:59 +0900 Subject: [PATCH 14/15] Use token_prefix_view in parser dispatch --- include/cli/parser.hh | 31 ++++++++++++++----------------- tests/test_tokenize.cc | 1 - 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/include/cli/parser.hh b/include/cli/parser.hh index 05dfafa..6e69737 100644 --- a/include/cli/parser.hh +++ b/include/cli/parser.hh @@ -397,12 +397,15 @@ inline auto token_prefix_view(const Token& tok, const TokenizerConfig& cfg) * - After the end-of-options separator (`--`), all remaining tokens become * `{positional, "...", i}` regardless of prefix. * - * @param args The argument span to tokenize (typically `argv[1:]`). - * @param spec_map Map from full option keys (e.g. `"--verbose"`, `"-v"`) to - * their `Nargs`. Must NOT contain command names. - * @param command_names Set of bare subcommand name strings (no prefix). - * @param cfg Tokenizer configuration (prefixes, separator character, - * etc.). + * @param args The argument span to tokenize (typically `argv[1:]`). + * @param option_nargs Callable that accepts a full option spelling (e.g. + * `"--verbose"`, `"-v"`) and returns its `Nargs`, or + * `std::nullopt` when unknown. Must NOT match command + * names. + * @param is_command Callable that accepts a bare token and returns whether it + * is a subcommand name. + * @param cfg Tokenizer configuration (prefixes, separator character, + * etc.). * @return A `TokenizeResult` containing the token list on success, or a * `ParseError` on failure. */ @@ -412,7 +415,7 @@ inline auto tokenize(std::span args, const TokenizerConfig& cfg = {}) -> TokenizeResult { TokenizeResult result; - // Returns the matched prefix (as a view into cfg.option_prefixes) or empty. + // Returns an encoded prefix tag that can be decoded with token_prefix_view(). const auto find_prefix = [&](std::string_view tok) -> std::uint8_t { for (std::uint8_t pi = 0; pi < static_cast(cfg.option_prefixes().size()); ++pi) { @@ -891,12 +894,6 @@ struct Parser { return; } - const auto prefix_view = [&](std::uint8_t p) -> std::string_view { - if (p == no_prefix()) return {}; - if (is_short_prefix(p)) return cfg_.short_option_prefix(); - return cfg_.option_prefixes()[p & kPrefixIndexMask]; - }; - const auto& tokens = tokenized.tokens; std::size_t pos_idx = 0, pos_cnt = 0; @@ -906,10 +903,10 @@ struct Parser { if (tok.type == TokenType::option) { std::size_t j = i + 1; while (j < tokens.size() && tokens[j].type == TokenType::value) ++j; - auto err = - dispatch_option(result.value, prefix_view(tok.prefix), tok.text, - std::span(tokens).subspan(i + 1, j - i - 1), - first_index, tok.position); + auto err = dispatch_option( + result.value, token_prefix_view(tok, cfg_), tok.text, + std::span(tokens).subspan(i + 1, j - i - 1), first_index, + tok.position); if (err.has_error()) { result.error = finalize_special_error(std::move(err.error)); return; diff --git a/tests/test_tokenize.cc b/tests/test_tokenize.cc index 4d3ebd8..c815736 100644 --- a/tests/test_tokenize.cc +++ b/tests/test_tokenize.cc @@ -482,7 +482,6 @@ TEST(TokenizeMultiPrefix, AltPrefixRecognised) { } TEST(TokenizeMultiPrefix, MatchedPrefixDistinguishesPrefixes) { - // "--foo" and "+foo" are different options; matched_prefix tells them apart auto cfg = cli::TokenizerConfig{}.with_option_prefixes<"--", "+">(); SpecMap spec{{"--foo", kFlag}, {"+foo", kFlag}}; auto r = tokenize_cfg({"--foo", "+foo"}, spec, cfg); From 1e523bd55602f0f774f7b8bfb34d54ac0b28bdbd Mon Sep 17 00:00:00 2001 From: gen740 <54583542+gen740@users.noreply.github.com> Date: Wed, 13 May 2026 02:28:41 +0900 Subject: [PATCH 15/15] Restore tokenizer test comment --- include/cli/parser.hh | 8 ++++---- tests/test_tokenize.cc | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/include/cli/parser.hh b/include/cli/parser.hh index 6e69737..28c233b 100644 --- a/include/cli/parser.hh +++ b/include/cli/parser.hh @@ -903,10 +903,10 @@ struct Parser { if (tok.type == TokenType::option) { std::size_t j = i + 1; while (j < tokens.size() && tokens[j].type == TokenType::value) ++j; - auto err = dispatch_option( - result.value, token_prefix_view(tok, cfg_), tok.text, - std::span(tokens).subspan(i + 1, j - i - 1), first_index, - tok.position); + auto err = + dispatch_option(result.value, token_prefix_view(tok, cfg_), tok.text, + std::span(tokens).subspan(i + 1, j - i - 1), + first_index, tok.position); if (err.has_error()) { result.error = finalize_special_error(std::move(err.error)); return; diff --git a/tests/test_tokenize.cc b/tests/test_tokenize.cc index c815736..4d3ebd8 100644 --- a/tests/test_tokenize.cc +++ b/tests/test_tokenize.cc @@ -482,6 +482,7 @@ TEST(TokenizeMultiPrefix, AltPrefixRecognised) { } TEST(TokenizeMultiPrefix, MatchedPrefixDistinguishesPrefixes) { + // "--foo" and "+foo" are different options; matched_prefix tells them apart auto cfg = cli::TokenizerConfig{}.with_option_prefixes<"--", "+">(); SpecMap spec{{"--foo", kFlag}, {"+foo", kFlag}}; auto r = tokenize_cfg({"--foo", "+foo"}, spec, cfg);