diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 2c81e6c..001242f 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -59,6 +59,9 @@ install(FILES include/jsonlogic/details/ast-full.hpp DESTINATION "include/jsonlo if(JSONLOGIC_ENABLE_BENCH) message(STATUS "Building benchmarks: ${JSONLOGIC_ENABLE_BENCH}") add_subdirectory(bench) + add_custom_target(bench + DEPENDS jl-bench-eq jl-bench-membership jl-bench-generic + ) endif() if(JSONLOGIC_ENABLE_TESTS) message(STATUS "Building tests: ${JSONLOGIC_ENABLE_TESTS}") diff --git a/cpp/README.md b/cpp/README.md index 5a18b36..04094e8 100644 --- a/cpp/README.md +++ b/cpp/README.md @@ -8,9 +8,20 @@ The library is designed to follow the type conversion rules of the reference Jso ## Compile and Install The library can be installed using cmake. From the top-level directory, - cmake --preset=default - cd build/release - make +``` +cmake --preset=default +cd build/release +make +``` +Benchmarks can be made with +``` +make bench +``` + +Tests can be run with +``` +make testeval && make test +``` ## Use diff --git a/cpp/bench/CMakeLists.txt b/cpp/bench/CMakeLists.txt index e98b71c..31e4a8f 100644 --- a/cpp/bench/CMakeLists.txt +++ b/cpp/bench/CMakeLists.txt @@ -3,6 +3,8 @@ # project(jl-bench-eqs) # set(CMAKE_BUILD_TYPE Debug) +include(FetchContent) + set(BUILD_TESTING OFF) # Disable Faker tests FetchContent_Declare(faker GIT_REPOSITORY https://github.com/cieslarmichal/faker-cxx.git @@ -11,6 +13,13 @@ FetchContent_Declare(faker FetchContent_MakeAvailable(faker) +FetchContent_Declare( + cxxopts + GIT_REPOSITORY https://github.com/jarro2783/cxxopts.git + GIT_TAG v3.3.1 +) +FetchContent_MakeAvailable(cxxopts) + add_executable(jl-bench-eq src/benchmark-equality.cpp) add_compile_options(-Wall -Wextra -Wunused-variable) @@ -64,22 +73,20 @@ target_link_libraries(jl-bench-membership PRIVATE jsonlogic faker-cxx) # ${CMAKE_BINARY_DIR}/_deps/faker-src/include # ) -add_executable(jl-bench-complex1 src/benchmark-complex1.cpp) -target_compile_options(jl-bench-complex1 PRIVATE -O3) -target_include_directories(jl-bench-complex1 SYSTEM PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/../bench/include - ${CMAKE_CURRENT_SOURCE_DIR}/../include -) - -configure_file(${CMAKE_SOURCE_DIR}/bench/src/complex1.json ${CMAKE_BINARY_DIR}/bench/complex1.json COPYONLY) -target_link_libraries(jl-bench-complex1 PRIVATE jsonlogic faker-cxx) -add_executable(jl-bench-simple-and src/benchmark-simple-and.cpp) -target_compile_options(jl-bench-simple-and PRIVATE -O3) -target_include_directories(jl-bench-simple-and SYSTEM PRIVATE +add_executable(jl-bench-generic src/benchmark-generic.cpp) +target_include_directories(jl-bench-generic SYSTEM PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../bench/include ${CMAKE_CURRENT_SOURCE_DIR}/../include ) -configure_file(${CMAKE_SOURCE_DIR}/bench/src/simple-and.json ${CMAKE_BINARY_DIR}/bench/simple-and.json COPYONLY) -target_link_libraries(jl-bench-simple-and PRIVATE jsonlogic faker-cxx) +target_link_libraries(jl-bench-generic PRIVATE faker-cxx jsonlogic cxxopts) +target_compile_features(jl-bench-generic PRIVATE cxx_std_20) +target_compile_options(jl-bench-generic PRIVATE -O3) + +# Copy all .json files from bench/src to the build directory's bench folder +file(GLOB BENCH_JSON_FILES "${CMAKE_SOURCE_DIR}/bench/src/*.json") +foreach(jsonfile ${BENCH_JSON_FILES}) + get_filename_component(jsonfile_name "${jsonfile}" NAME) + configure_file(${jsonfile} ${CMAKE_BINARY_DIR}/bench/${jsonfile_name} COPYONLY) +endforeach() diff --git a/cpp/bench/src/benchmark-complex1.cpp b/cpp/bench/src/benchmark-complex1.cpp deleted file mode 100644 index c8acde8..0000000 --- a/cpp/bench/src/benchmark-complex1.cpp +++ /dev/null @@ -1,150 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -std::string read_file(const std::string &filename) { - std::ifstream file(filename); - if (!file) - throw std::runtime_error("Failed to open file"); - - return {std::string((std::istreambuf_iterator(file)), - std::istreambuf_iterator())}; -} - -const unsigned long SEED_ = 42; -static const size_t N_ = 1'000'000; -static const int N_RUNS_ = 3; -int main(int argc, const char **argv) try { - - // x: double - // y: int - // z: string - // (x / y > 5) or (x < 3.0 and y > 5 and z == "foo") or (y == 4 and x > 10.0 - // and "bar" in z) - std::string expr; - try { - expr = read_file("complex1.json"); - std::cout << "Successfully read complex1.json from current directory" - << std::endl; - } catch (const std::exception &) { - try { - expr = read_file("bench/src/complex1.json"); - std::cout << "Successfully read complex1.json from bench/src/" - << std::endl; - } catch (const std::exception &e) { - std::cerr << "Error: Could not find complex1.json: " << e.what() - << std::endl; - // Print current working directory - std::cerr << "Current directory: " << std::filesystem::current_path() - << std::endl; - - throw; - } - } - - std::span args(argv, argc); - - size_t N = N_; - if (argc > 1) { - N = std::stoul(args[1]); - } - size_t N_RUNS = N_RUNS_; - if (argc > 2) { - N_RUNS = std::stoul(args[2]); - } - - size_t SEED = SEED_; - if (argc > 3) { - SEED = std::stoul(args[3]); - } - - faker::getGenerator().seed(SEED); - std::vector xs; - xs.reserve(N); - std::vector ys; - ys.reserve(N); - - std::vector zs; - zs.reserve(N); - - std::vector strset{"foo", "bar", "baz", "quux", "foobar"}; - - // Create data - for (size_t i = 0; i < N; ++i) { - xs.push_back(faker::number::decimal(0, 50)); - ys.push_back(faker::number::integer(0, 255)); - auto ind = faker::number::integer(0, strset.size() - 1); - zs.push_back(strset[ind]); - } - - // JL 1 - - // Create jsonlogic benchmark - auto jv_xy = boost::json::parse(expr); - boost::json::object data_obj; - - size_t matches = 0; - auto jl_lambda = [&] { - matches = 0; - for (size_t i = 0; i < N; ++i) { - data_obj["x"] = xs[i]; - data_obj["y"] = ys[i]; - data_obj["z"] = zs[i]; - auto data = boost::json::value_from(data_obj); - auto v_xy = jsonlogic::apply(jv_xy, data); - - bool val = jsonlogic::truthy(v_xy); - - if (val) { - ++matches; - } - } - }; - - auto jl_bench = Benchmark("2ints-jl1", jl_lambda); - - // JL 2 - - auto jl2_lambda = [&] { - matches = 0; - auto [rule, ignore1, igmore2] = jsonlogic::create_logic(jv_xy); - for (size_t i = 0; i < N; ++i) { - auto v_xy = jsonlogic::apply(rule, {xs[i], ys[i], zs[i]}); - bool val = jsonlogic::truthy(v_xy); - - if (val) { - ++matches; - } - } - }; - - auto jl2_bench = Benchmark("2ints-jl2", jl2_lambda); - - auto jl_results = jl_bench.run(N_RUNS); - std::cout << "- jl1 matches: " << matches << std::endl; - auto jl2_results = jl2_bench.run(N_RUNS); - std::cout << "jl2 matches: " << matches << std::endl; - - jl_results.summarize(); - jl2_results.summarize(); - //~ jl_results.compare_to(cpp_results); - //~ cpp_results.compare_to(jl_results); - - jl2_results.compare_to(jl_results); - return 0; -} catch (const std::exception &e) { - std::cerr << "Fatal error: " << e.what() << '\n'; - return 1; -} catch (...) { - std::cerr << "Fatal unkown error\n"; - return 2; -} diff --git a/cpp/bench/src/benchmark-generic.cpp b/cpp/bench/src/benchmark-generic.cpp new file mode 100644 index 0000000..8d9d66e --- /dev/null +++ b/cpp/bench/src/benchmark-generic.cpp @@ -0,0 +1,171 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; +namespace bjsn = boost::json; + +std::string read_file(const std::string &filename) { + std::ifstream file(filename); + if (!file) + throw std::runtime_error("Failed to open file: " + filename); + return {std::istreambuf_iterator(file), + std::istreambuf_iterator()}; +} + +// Supported types: 'i' = int, 'd' = double, 's' = string, 'b' = bool +// Use a char-based enum for type codes +enum class VarType : char { Int = 'i', Double = 'd', String = 's', Bool = 'b' }; + +using VarValue = std::variant; + +VarValue fake_value(VarType type) { + switch (type) { + case VarType::Int: + return faker::number::integer(0, 10); + case VarType::Double: + return faker::number::decimal(0, 10); + case VarType::String: + return faker::string::alphanumeric(); + case VarType::Bool: + return faker::number::integer(0, 1) != 0; + default: + throw std::runtime_error(std::string("Unknown VarType: '") + + static_cast(type) + "')"); + } +} + +int main(int argc, const char **argv) try { + cxxopts::Options options("benchmark-generic", "Generic JSONLogic benchmark"); + options.add_options()("f,file", "Input JSON file", + cxxopts::value())( + "n,nrows", "Number of data rows", + cxxopts::value()->default_value("10000"))( + "r,runs", "Number of runs", cxxopts::value()->default_value("3"))( + "s,seed", "Random seed", + cxxopts::value()->default_value("42"))("h,help", "Print usage"); + + auto result = options.parse(argc, argv); + if (result.count("help") || !result.count("file")) { + std::cout << options.help() << std::endl; + return 0; + } + std::string jsonfile = result["file"].as(); + size_t N = result["nrows"].as(); + size_t N_RUNS = result["runs"].as(); + size_t SEED = result["seed"].as(); + faker::getGenerator().seed(SEED); + + // Read and parse input JSON + auto j = bjsn::parse(read_file(jsonfile)); + if (!j.is_object()) + throw std::runtime_error("Input JSON must be an object"); + const auto &obj = j.as_object(); + if (!obj.contains("rule") || !obj.contains("types")) + throw std::runtime_error("Input JSON must contain 'rule' and 'types'"); + + const auto &rule = obj.at("rule"); + const auto &types = obj.at("types").as_object(); + + // Prepare variable names and types + std::vector var_names; + std::vector var_types; + for (const auto &kv : types) { + var_names.push_back(kv.key_c_str()); + // Extract the type code as a char from the JSON string value (must be a + // single-character string) + const auto &type_str = kv.value().as_string(); + if (type_str.empty()) { + throw std::runtime_error(std::string("Type string for variable '") + + kv.key_c_str() + "' is empty"); + } + var_types.push_back(static_cast(type_str.front())); + } + + // Generate fake data for each variable + std::vector> data(var_names.size()); + for (size_t i = 0; i < var_names.size(); ++i) { + data[i].reserve(N); + for (size_t j = 0; j < N; ++j) { + data[i].push_back(fake_value(var_types[i])); + } + } + + // JL1: Use boost::json::object for each row + size_t matches = 0; + auto jl1_lambda = [&] { + matches = 0; + boost::json::object data_obj; + for (size_t i = 0; i < N; ++i) { + for (size_t v = 0; v < var_names.size(); ++v) { + const auto &val = data[v][i]; + if (std::holds_alternative(val)) + data_obj[var_names[v]] = std::get(val); + else if (std::holds_alternative(val)) + data_obj[var_names[v]] = std::get(val); + else if (std::holds_alternative(val)) + data_obj[var_names[v]] = std::get(val); + else if (std::holds_alternative(val)) + data_obj[var_names[v]] = std::get(val); + } + auto result = jsonlogic::apply(rule, bjsn::value_from(data_obj)); + bool val = jsonlogic::truthy(result); + if (val) + ++matches; + } + }; + auto jl1_bench = Benchmark("generic-jl1", jl1_lambda); + + // JL2: Use create_logic and pass values as tuple + auto jl2_lambda = [&] { + matches = 0; + auto [logic_rule, ignore1, ignore2] = jsonlogic::create_logic(rule); + for (size_t i = 0; i < N; ++i) { + std::vector args; + for (size_t v = 0; v < var_names.size(); ++v) { + const auto &val = data[v][i]; + if (std::holds_alternative(val)) + args.push_back(std::get(val)); + else if (std::holds_alternative(val)) + args.push_back(std::get(val)); + else if (std::holds_alternative(val)) + args.push_back(std::get(val)); + else if (std::holds_alternative(val)) + args.push_back(std::get(val)); + } + auto result = jsonlogic::apply(logic_rule, args); + bool val = jsonlogic::truthy(result); + if (val) + ++matches; + } + }; + auto jl2_bench = Benchmark("generic-jl2", jl2_lambda); + + // Run benchmarks + auto jl1_results = jl1_bench.run(N_RUNS); + std::cout << "JL1 matches: " << matches << std::endl; + auto jl2_results = jl2_bench.run(N_RUNS); + std::cout << "JL2 matches: " << matches << std::endl; + jl1_results.summarize(); + jl2_results.summarize(); + jl2_results.compare_to(jl1_results); + return 0; +} catch (const std::exception &e) { + std::cerr << "Fatal error: " << e.what() << '\n'; + return 1; +} catch (...) { + std::cerr << "Fatal unknown error\n"; + return 2; +} diff --git a/cpp/bench/src/benchmark-simple-and.cpp b/cpp/bench/src/benchmark-simple-and.cpp deleted file mode 100644 index 2c59264..0000000 --- a/cpp/bench/src/benchmark-simple-and.cpp +++ /dev/null @@ -1,140 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -std::string read_file(const std::string &filename) { - std::ifstream file(filename); - if (!file) - throw std::runtime_error("Failed to open file"); - - return {std::string((std::istreambuf_iterator(file)), - std::istreambuf_iterator())}; -} - -const unsigned long SEED_ = 42; -static const size_t N_ = 1'000'000; -static const int N_RUNS_ = 3; - -int main(int argc, const char **argv) try { - - // Test expression: x > 5 and y < 3 - std::string expr; - try { - expr = read_file("simple-and.json"); - std::cout << "Successfully read simple-and.json from current directory" - << std::endl; - } catch (const std::exception &) { - try { - expr = read_file("bench/src/simple-and.json"); - std::cout << "Successfully read simple-and.json from bench/src/" - << std::endl; - } catch (const std::exception &e) { - std::cerr << "Error: Could not find simple-and.json: " << e.what() - << std::endl; - // Print current working directory - std::cerr << "Current directory: " << std::filesystem::current_path() - << std::endl; - throw; - } - } - - std::span args(argv, argc); - - size_t N = N_; - if (argc > 1) { - N = std::stoul(args[1]); - } - size_t N_RUNS = N_RUNS_; - if (argc > 2) { - N_RUNS = std::stoul(args[2]); - } - - size_t SEED = SEED_; - if (argc > 3) { - SEED = std::stoul(args[3]); - } - - faker::getGenerator().seed(SEED); - std::vector xs; - xs.reserve(N); - std::vector ys; - ys.reserve(N); - - // Create test data - for (size_t i = 0; i < N; ++i) { - xs.push_back(faker::number::decimal( - 0, 10)); // 0-10 range to get some matches - ys.push_back( - faker::number::integer(0, 6)); // 0-6 range to get some matches - } - - // JL1 - Using boost::json::object approach - - auto jv_expr = boost::json::parse(expr); - boost::json::object data_obj; - - size_t matches = 0; - auto jl1_lambda = [&] { - matches = 0; - for (size_t i = 0; i < N; ++i) { - data_obj["x"] = xs[i]; - data_obj["y"] = ys[i]; - auto data = boost::json::value_from(data_obj); - auto result = jsonlogic::apply(jv_expr, data); - - bool val = jsonlogic::truthy(result); - - if (val) { - ++matches; - } - } - }; - - auto jl1_bench = Benchmark("simple-and-jl1", jl1_lambda); - - // JL2 - Using create_logic approach - - auto jl2_lambda = [&] { - matches = 0; - auto [rule, ignore1, ignore2] = jsonlogic::create_logic(jv_expr); - for (size_t i = 0; i < N; ++i) { - auto result = jsonlogic::apply(rule, {xs[i], ys[i]}); - bool val = jsonlogic::truthy(result); - - if (val) { - ++matches; - } - } - }; - - auto jl2_bench = Benchmark("simple-and-jl2", jl2_lambda); - - // Run benchmarks - auto jl1_results = jl1_bench.run(N_RUNS); - std::cout << "JL1 matches: " << matches << std::endl; - - auto jl2_results = jl2_bench.run(N_RUNS); - std::cout << "JL2 matches: " << matches << std::endl; - - // Display results - jl1_results.summarize(); - jl2_results.summarize(); - jl2_results.compare_to(jl1_results); - - return 0; -} catch (const std::exception &e) { - std::cerr << "Fatal error: " << e.what() << '\n'; - return 1; -} catch (...) { - std::cerr << "Fatal unknown error\n"; - return 2; -} diff --git a/cpp/bench/src/complex1.json b/cpp/bench/src/complex1.json index bfdc1fd..eab75ac 100644 --- a/cpp/bench/src/complex1.json +++ b/cpp/bench/src/complex1.json @@ -1,33 +1,23 @@ -{"or": [ - {">": [ - {"/": [ - {"var": "x"}, - {"var": "y" } - ]}, - 5 - ]}, - {"and": [ - {"<": [ - {"var": "x"}, - 3.0 - ]}, - {">": [ - {"var": "y"}, - 5 - ]}, - {"==": [ - {"var": "z"}, - "foo" - ]} - ]}, - {"or": [ - {"==": [ - {"var": "y"}, - 4 - ]}, - {">": [ - {"var": "x"}, - 10.0 - ]} - ]} -]} \ No newline at end of file +{ + "rule": { + "or": [ + { ">": [ { "/": [ { "var": "x" }, { "var": "y" } ] }, 5 ] }, + { "and": [ + { "<": [ { "var": "x" }, 3.0 ] }, + { ">": [ { "var": "y" }, 5 ] }, + { "==": [ { "var": "z" }, "foo" ] } + ] + }, + { "or": [ + { "==": [ { "var": "y" }, 4 ] }, + { ">": [ { "var": "x" }, 10.0 ] } + ] + } + ] + }, + "types": { + "x": "d", + "y": "i", + "z": "s" + } +} \ No newline at end of file diff --git a/cpp/bench/src/equality.json b/cpp/bench/src/equality.json new file mode 100644 index 0000000..4d2999f --- /dev/null +++ b/cpp/bench/src/equality.json @@ -0,0 +1,7 @@ +{ + "rule": {"==": [ {"var": "x"}, {"var": "y"} ]}, + "types": { + "x": "i", + "y": "i" + } +} diff --git a/cpp/bench/src/simple-and.json b/cpp/bench/src/simple-and.json index fb5ad01..d19579d 100644 --- a/cpp/bench/src/simple-and.json +++ b/cpp/bench/src/simple-and.json @@ -1,10 +1,12 @@ -{"and": [ - {">": [ - {"var": "x"}, - 5 - ]}, - {"<": [ - {"var": "y"}, - 3 - ]} -]} +{ + "rule": { + "and": [ + {">": [ {"var": "x"}, 5 ]}, + {"<": [ {"var": "y"}, 3 ]} + ] + }, + "types": { + "x": "double", + "y": "int" + } +}