diff --git a/.github/workflows/cpp-linter.yml b/.github/workflows/cpp-linter.yml index 3dcf7ba4f..07e2e8b22 100644 --- a/.github/workflows/cpp-linter.yml +++ b/.github/workflows/cpp-linter.yml @@ -76,6 +76,7 @@ jobs: cmake .. -G Ninja -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ -DCMAKE_C_COMPILER_LAUNCHER=sccache \ -DCMAKE_CXX_COMPILER_LAUNCHER=sccache \ + -DICEBERG_BUILD_BENCHMARKS=ON \ -DICEBERG_BUILD_SQL_CATALOG=ON \ -DICEBERG_SQL_SQLITE=ON \ -DICEBERG_SQL_POSTGRESQL=ON \ diff --git a/CMakeLists.txt b/CMakeLists.txt index 5f8823567..db54b24ed 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,6 +40,7 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) option(ICEBERG_BUILD_STATIC "Build static library" ON) option(ICEBERG_BUILD_SHARED "Build shared library" OFF) option(ICEBERG_BUILD_TESTS "Build tests" ON) +option(ICEBERG_BUILD_BENCHMARKS "Build benchmarks" OFF) option(ICEBERG_BUILD_BUNDLE "Build the battery included library" ON) option(ICEBERG_BUILD_REST "Build rest catalog client" ON) option(ICEBERG_BUILD_REST_INTEGRATION_TESTS "Build rest catalog integration tests" OFF) diff --git a/src/iceberg/CMakeLists.txt b/src/iceberg/CMakeLists.txt index 999532c1c..a9dd4e82a 100644 --- a/src/iceberg/CMakeLists.txt +++ b/src/iceberg/CMakeLists.txt @@ -361,3 +361,7 @@ endif() if(ICEBERG_BUILD_TESTS) add_subdirectory(test) endif() + +if(ICEBERG_BUILD_BENCHMARKS) + add_subdirectory(benchmark) +endif() diff --git a/src/iceberg/benchmark/CMakeLists.txt b/src/iceberg/benchmark/CMakeLists.txt new file mode 100644 index 000000000..833171715 --- /dev/null +++ b/src/iceberg/benchmark/CMakeLists.txt @@ -0,0 +1,40 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +set(BENCHMARK_ENABLE_TESTING + OFF + CACHE BOOL "" FORCE) +set(BENCHMARK_ENABLE_GTEST_TESTS + OFF + CACHE BOOL "" FORCE) +set(BENCHMARK_ENABLE_INSTALL + OFF + CACHE BOOL "" FORCE) + +fetchcontent_declare(googlebenchmark + GIT_REPOSITORY https://github.com/google/benchmark.git + GIT_TAG v1.9.1 + FIND_PACKAGE_ARGS + NAMES + benchmark) + +fetchcontent_makeavailable(googlebenchmark) + +add_executable(strict_metrics_evaluator_bench strict_metrics_evaluator_bench.cc) +target_link_libraries(strict_metrics_evaluator_bench + PRIVATE benchmark::benchmark benchmark::benchmark_main + iceberg_static) diff --git a/src/iceberg/benchmark/strict_metrics_evaluator_bench.cc b/src/iceberg/benchmark/strict_metrics_evaluator_bench.cc new file mode 100644 index 000000000..0611f23ce --- /dev/null +++ b/src/iceberg/benchmark/strict_metrics_evaluator_bench.cc @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include +#include + +#include + +#include "iceberg/expression/expressions.h" +#include "iceberg/expression/strict_metrics_evaluator.h" +#include "iceberg/manifest/manifest_entry.h" +#include "iceberg/schema.h" +#include "iceberg/type.h" + +namespace iceberg { +namespace { + +std::shared_ptr MakeSchema() { + return std::make_shared( + std::vector{ + SchemaField::MakeRequired(1, "id", int64()), + SchemaField::MakeOptional(2, "name", string()), + SchemaField::MakeRequired(3, "age", int32()), + SchemaField::MakeOptional(4, "salary", float64()), + SchemaField::MakeRequired(5, "active", boolean()), + SchemaField::MakeRequired(6, "date", string()), + }, + /*schema_id=*/0); +} + +std::shared_ptr MakeDataFile() { + auto data_file = std::make_shared(); + data_file->file_path = "bench_path"; + data_file->file_format = FileFormatType::kParquet; + data_file->partition.AddValue(Literal::String("20251128")); + data_file->record_count = 10; + data_file->file_size_in_bytes = 1024; + data_file->value_counts = {{1, 10}, {3, 10}, {5, 10}}; + data_file->null_value_counts = {{1, 0}, {3, 0}, {5, 0}}; + data_file->lower_bounds[1] = Literal::Long(100).Serialize().value(); + data_file->upper_bounds[1] = Literal::Long(200).Serialize().value(); + data_file->lower_bounds[3] = Literal::Int(20).Serialize().value(); + data_file->upper_bounds[3] = Literal::Int(40).Serialize().value(); + data_file->lower_bounds[5] = Literal::Boolean(true).Serialize().value(); + data_file->upper_bounds[5] = Literal::Boolean(true).Serialize().value(); + return data_file; +} + +std::shared_ptr MakeFilter(int predicate_count) { + std::vector> predicates; + predicates.reserve(predicate_count); + for (int i = 0; i < predicate_count; ++i) { + switch (i % 6) { + case 0: + predicates.push_back(Expressions::GreaterThan("id", Literal::Long(50))); + break; + case 1: + predicates.push_back(Expressions::LessThan("id", Literal::Long(300))); + break; + case 2: + predicates.push_back(Expressions::GreaterThanOrEqual("age", Literal::Int(18))); + break; + case 3: + predicates.push_back(Expressions::LessThanOrEqual("age", Literal::Int(65))); + break; + case 4: + predicates.push_back(Expressions::Equal("active", Literal::Boolean(true))); + break; + default: + predicates.push_back(Expressions::NotNull("id")); + break; + } + } + + std::shared_ptr filter = std::move(predicates.front()); + for (size_t i = 1; i < predicates.size(); ++i) { + filter = Expressions::And(filter, predicates[i]); + } + return filter; +} + +void BM_StrictMetricsEvaluate(benchmark::State& state) { + auto schema = MakeSchema(); + auto data_file = MakeDataFile(); + auto filter = MakeFilter(static_cast(state.range(0))); + auto evaluator_result = StrictMetricsEvaluator::Make(filter, schema, true); + if (!evaluator_result.has_value()) { + state.SkipWithError(evaluator_result.error().message.c_str()); + return; + } + auto& evaluator = *evaluator_result.value(); + + for (auto _ : state) { + auto result = evaluator.Evaluate(*data_file); + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations()); +} + +} // namespace +} // namespace iceberg + +BENCHMARK(iceberg::BM_StrictMetricsEvaluate)->Arg(1)->Arg(14)->Arg(20); +BENCHMARK_MAIN(); diff --git a/src/iceberg/expression/strict_metrics_evaluator.cc b/src/iceberg/expression/strict_metrics_evaluator.cc index 8f6d27876..d92694a36 100644 --- a/src/iceberg/expression/strict_metrics_evaluator.cc +++ b/src/iceberg/expression/strict_metrics_evaluator.cc @@ -40,9 +40,10 @@ constexpr bool kRowsMightNotMatch = false; // For example, bucket16(x) = 0 can't be determined because this visitor operates on data // metrics and not partition values. It may be possible to un-transform expressions for // order preserving transforms in the future, but this is not currently supported. -#define RETURN_IF_NOT_REFERENCE(expr) \ - if (auto ref = dynamic_cast(expr.get()); ref == nullptr) { \ - return kRowsMightNotMatch; \ +#define BIND_REFERENCE_OR_RETURN(ref, expr) \ + const auto* ref = dynamic_cast((expr).get()); \ + if (ref == nullptr) { \ + return kRowsMightNotMatch; \ } class StrictMetricsVisitor : public BoundVisitor { @@ -65,11 +66,11 @@ class StrictMetricsVisitor : public BoundVisitor { } Result IsNull(const std::shared_ptr& expr) override { - RETURN_IF_NOT_REFERENCE(expr); + BIND_REFERENCE_OR_RETURN(ref, expr); // no need to check whether the field is required because binding evaluates that case // if the column has any non-null values, the expression does not match - int32_t id = expr->reference()->field().field_id(); + int32_t id = ref->field().field_id(); ICEBERG_ASSIGN_OR_RAISE(auto is_nested, IsNestedColumn(id)); if (is_nested) { @@ -83,11 +84,11 @@ class StrictMetricsVisitor : public BoundVisitor { } Result NotNull(const std::shared_ptr& expr) override { - RETURN_IF_NOT_REFERENCE(expr); + BIND_REFERENCE_OR_RETURN(ref, expr); // no need to check whether the field is required because binding evaluates that case // if the column has any null values, the expression does not match - int32_t id = expr->reference()->field().field_id(); + int32_t id = ref->field().field_id(); ICEBERG_ASSIGN_OR_RAISE(auto is_nested, IsNestedColumn(id)); if (is_nested) { @@ -103,9 +104,9 @@ class StrictMetricsVisitor : public BoundVisitor { } Result IsNaN(const std::shared_ptr& expr) override { - RETURN_IF_NOT_REFERENCE(expr); + BIND_REFERENCE_OR_RETURN(ref, expr); - int32_t id = expr->reference()->field().field_id(); + int32_t id = ref->field().field_id(); if (ContainsNaNsOnly(id)) { return kRowsMustMatch; @@ -115,9 +116,9 @@ class StrictMetricsVisitor : public BoundVisitor { } Result NotNaN(const std::shared_ptr& expr) override { - RETURN_IF_NOT_REFERENCE(expr); + BIND_REFERENCE_OR_RETURN(ref, expr); - int32_t id = expr->reference()->field().field_id(); + int32_t id = ref->field().field_id(); auto it = data_file_.nan_value_counts.find(id); if (it != data_file_.nan_value_counts.cend() && it->second == 0) { @@ -132,10 +133,10 @@ class StrictMetricsVisitor : public BoundVisitor { } Result Lt(const std::shared_ptr& expr, const Literal& lit) override { - RETURN_IF_NOT_REFERENCE(expr); + BIND_REFERENCE_OR_RETURN(ref, expr); // Rows must match when: <----------Min----Max---X-------> - int32_t id = expr->reference()->field().field_id(); + int32_t id = ref->field().field_id(); ICEBERG_ASSIGN_OR_RAISE(auto is_nested, IsNestedColumn(id)); if (is_nested) { @@ -160,10 +161,10 @@ class StrictMetricsVisitor : public BoundVisitor { } Result LtEq(const std::shared_ptr& expr, const Literal& lit) override { - RETURN_IF_NOT_REFERENCE(expr); + BIND_REFERENCE_OR_RETURN(ref, expr); // Rows must match when: <----------Min----Max---X-------> - int32_t id = expr->reference()->field().field_id(); + int32_t id = ref->field().field_id(); ICEBERG_ASSIGN_OR_RAISE(auto is_nested, IsNestedColumn(id)); if (is_nested) { @@ -188,10 +189,10 @@ class StrictMetricsVisitor : public BoundVisitor { } Result Gt(const std::shared_ptr& expr, const Literal& lit) override { - RETURN_IF_NOT_REFERENCE(expr); + BIND_REFERENCE_OR_RETURN(ref, expr); // Rows must match when: <-------X---Min----Max----------> - int32_t id = expr->reference()->field().field_id(); + int32_t id = ref->field().field_id(); ICEBERG_ASSIGN_OR_RAISE(auto is_nested, IsNestedColumn(id)); if (is_nested) { @@ -222,10 +223,10 @@ class StrictMetricsVisitor : public BoundVisitor { } Result GtEq(const std::shared_ptr& expr, const Literal& lit) override { - RETURN_IF_NOT_REFERENCE(expr); + BIND_REFERENCE_OR_RETURN(ref, expr); // Rows must match when: <-------X---Min----Max----------> - int32_t id = expr->reference()->field().field_id(); + int32_t id = ref->field().field_id(); ICEBERG_ASSIGN_OR_RAISE(auto is_nested, IsNestedColumn(id)); if (is_nested) { @@ -256,10 +257,10 @@ class StrictMetricsVisitor : public BoundVisitor { } Result Eq(const std::shared_ptr& expr, const Literal& lit) override { - RETURN_IF_NOT_REFERENCE(expr); + BIND_REFERENCE_OR_RETURN(ref, expr); // Rows must match when Min == X == Max - int32_t id = expr->reference()->field().field_id(); + int32_t id = ref->field().field_id(); ICEBERG_ASSIGN_OR_RAISE(auto is_nested, IsNestedColumn(id)); if (is_nested) { @@ -291,10 +292,10 @@ class StrictMetricsVisitor : public BoundVisitor { } Result NotEq(const std::shared_ptr& expr, const Literal& lit) override { - RETURN_IF_NOT_REFERENCE(expr); + BIND_REFERENCE_OR_RETURN(ref, expr); // Rows must match when X < Min or Max < X because it is not in the range - int32_t id = expr->reference()->field().field_id(); + int32_t id = ref->field().field_id(); ICEBERG_ASSIGN_OR_RAISE(auto is_nested, IsNestedColumn(id)); if (is_nested) { @@ -331,9 +332,9 @@ class StrictMetricsVisitor : public BoundVisitor { Result In(const std::shared_ptr& expr, const BoundSetPredicate::LiteralSet& literal_set) override { - RETURN_IF_NOT_REFERENCE(expr); + BIND_REFERENCE_OR_RETURN(ref, expr); - int32_t id = expr->reference()->field().field_id(); + int32_t id = ref->field().field_id(); ICEBERG_ASSIGN_OR_RAISE(auto is_nested, IsNestedColumn(id)); if (is_nested) { @@ -375,9 +376,9 @@ class StrictMetricsVisitor : public BoundVisitor { Result NotIn(const std::shared_ptr& expr, const BoundSetPredicate::LiteralSet& literal_set) override { - RETURN_IF_NOT_REFERENCE(expr); + BIND_REFERENCE_OR_RETURN(ref, expr); - int32_t id = expr->reference()->field().field_id(); + int32_t id = ref->field().field_id(); ICEBERG_ASSIGN_OR_RAISE(auto is_nested, IsNestedColumn(id)); if (is_nested) {