From cfb9bdceb0a9bf00b49f058ff64898807d4eff2f Mon Sep 17 00:00:00 2001 From: Jonathan Tatum Date: Tue, 27 Oct 2026 11:00:21 -0700 Subject: [PATCH] Add string set membership optimization for constant string lists. PiperOrigin-RevId: 1008230065 --- eval/eval/evaluator_core.h | 2 + extensions/BUILD | 6 + extensions/set_membership_optimization.cc | 388 +++++++++++++++++- extensions/set_membership_optimization.h | 15 +- .../set_membership_optimization_test.cc | 300 ++++++++++++-- extensions/sets_functions_benchmark_test.cc | 205 +++++++++ 6 files changed, 851 insertions(+), 65 deletions(-) diff --git a/eval/eval/evaluator_core.h b/eval/eval/evaluator_core.h index ceb7901d5..6f1ae2112 100644 --- a/eval/eval/evaluator_core.h +++ b/eval/eval/evaluator_core.h @@ -165,6 +165,8 @@ class ExpressionStep { return header_.id >= 0 ? static_cast(header_.id) : -1; } + ExpressionStepKind kind() const { return header_.kind; } + // Returns if the execution step comes from AST. bool comes_from_ast() const { return header_.id >= 0; } diff --git a/extensions/BUILD b/extensions/BUILD index b0a6c3d65..f6e5cab66 100644 --- a/extensions/BUILD +++ b/extensions/BUILD @@ -883,16 +883,22 @@ cc_library( "//common:expr", "//common:native_type", "//common:standard_definitions", + "//common:type", "//common:value", + "//common/internal:attribute_trail", "//eval/compiler:flat_expr_builder", "//eval/compiler:flat_expr_builder_extensions", "//eval/eval:evaluator_core", + "//eval/eval:expression_step_base", "//internal:casts", "//internal:status_macros", "//runtime", "//runtime:runtime_builder", + "//runtime/internal:errors", "//runtime/internal:runtime_friend_access", "//runtime/internal:runtime_impl", + "@com_google_absl//absl/container:flat_hash_set", + "@com_google_absl//absl/hash", "@com_google_absl//absl/log:absl_check", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", diff --git a/extensions/set_membership_optimization.cc b/extensions/set_membership_optimization.cc index 34805e041..dbb36176d 100644 --- a/extensions/set_membership_optimization.cc +++ b/extensions/set_membership_optimization.cc @@ -18,8 +18,11 @@ #include #include #include +#include #include +#include "absl/container/flat_hash_set.h" +#include "absl/hash/hash.h" #include "absl/log/absl_check.h" #include "absl/status/status.h" #include "absl/status/statusor.h" @@ -28,15 +31,19 @@ #include "common/ast.h" #include "common/constant.h" #include "common/expr.h" +#include "common/internal/attribute_trail.h" #include "common/native_type.h" #include "common/standard_definitions.h" +#include "common/type.h" #include "common/value.h" #include "eval/compiler/flat_expr_builder.h" #include "eval/compiler/flat_expr_builder_extensions.h" #include "eval/eval/bitset_steps.h" #include "eval/eval/evaluator_core.h" +#include "eval/eval/expression_step_base.h" #include "internal/casts.h" #include "internal/status_macros.h" +#include "runtime/internal/errors.h" #include "runtime/internal/runtime_friend_access.h" #include "runtime/internal/runtime_impl.h" #include "runtime/runtime.h" @@ -45,9 +52,12 @@ namespace cel::extensions { namespace { +using ::google::api::expr::runtime::ExecutionFrame; +using ::google::api::expr::runtime::ExecutionFrameBase; using ::google::api::expr::runtime::ExecutionPath; using ::google::api::expr::runtime::ExecutionPathView; using ::google::api::expr::runtime::ExpressionStep; +using ::google::api::expr::runtime::ExpressionStepBase; using ::google::api::expr::runtime::ExtractBit; using ::google::api::expr::runtime::FlatExprBuilder; using ::google::api::expr::runtime::GetIfConstant; @@ -67,6 +77,201 @@ constexpr absl::string_view kSetsIntersectsOverloadId = constexpr absl::string_view kSetsEquivalentOverloadId = "list_sets_equivalent_list"; +struct StringSetHash { + using is_transparent = void; + size_t operator()(absl::string_view v) const { return absl::HashOf(v); } + size_t operator()(const StringValue& v) const { return absl::HashOf(v); } +}; + +struct StringSetEq { + using is_transparent = void; + bool operator()(absl::string_view lhs, absl::string_view rhs) const { + return lhs == rhs; + } + bool operator()(absl::string_view lhs, const StringValue& rhs) const { + return lhs == rhs; + } + bool operator()(const StringValue& lhs, absl::string_view rhs) const { + return lhs == rhs; + } + bool operator()(const StringValue& lhs, const StringValue& rhs) const { + return lhs == rhs; + } +}; + +using StringSet = absl::flat_hash_set; + +enum class StringSetListOp { + kIntersects, + kContains, + kEquivalent, +}; + +absl::string_view StringSetListOpName(StringSetListOp op) { + switch (op) { + case StringSetListOp::kIntersects: + return kSetsIntersects; + case StringSetListOp::kContains: + return kSetsContains; + case StringSetListOp::kEquivalent: + return kSetsEquivalent; + } + return ""; +} + +class StringSetInStep final : public ExpressionStepBase { + public: + StringSetInStep(StringSet set, int64_t expr_id) + : ExpressionStepBase(expr_id), set_(std::move(set)) {} + + void Evaluate(ExecutionFrame* frame) const override { + if (!frame->value_stack().HasEnough(1)) { + frame->Abort(absl::InternalError("StringSetInStep: stack underflow")); + return; + } + const Value& arg = frame->value_stack().Peek(); + if (arg.IsError()) { + return; + } + if (frame->unknown_processing_enabled()) { + if (auto unknown = + cel::common_internal::PartiallyIdentifyAndMergeUnknowns( + frame->value_stack().GetSpan(1), + frame->value_stack().GetAttributeSpan(1), + frame->unknown_tree()); + unknown.has_value()) { + frame->value_stack().PopAndPush(1, *unknown); + return; + } + } + bool found = arg.IsString() && set_.contains(arg.GetString()); + frame->value_stack().PopAndPush(BoolValue(found)); + } + + private: + StringSet set_; +}; + +class StringSetListStep final : public ExpressionStepBase { + public: + StringSetListStep(StringSetListOp op, StringSet set, int64_t expr_id) + : ExpressionStepBase(expr_id), op_(op), set_(std::move(set)) {} + + void Evaluate(ExecutionFrame* frame) const override { + if (!frame->value_stack().HasEnough(1)) { + frame->Abort(absl::InternalError("StringSetListStep: stack underflow")); + return; + } + const Value& arg = frame->value_stack().Peek(); + if (arg.IsError()) { + return; + } + if (frame->unknown_processing_enabled()) { + if (auto unknown = + cel::common_internal::PartiallyIdentifyAndMergeUnknowns( + frame->value_stack().GetSpan(1), + frame->value_stack().GetAttributeSpan(1), + frame->unknown_tree()); + unknown.has_value()) { + frame->value_stack().PopAndPush(1, *unknown); + return; + } + } + + if (!arg.IsList()) { + frame->value_stack().PopAndPush(cel::ErrorValue::From( + cel::runtime_internal::CreateNoMatchingOverloadError( + StringSetListOpName(op_)), + frame->arena())); + return; + } + + const ListValue& list = arg.GetList(); + absl::StatusOr result = EvaluateList(*frame, list); + if (!result.ok()) { + frame->Abort(std::move(result).status()); + return; + } + frame->value_stack().PopAndPush(BoolValue(*result)); + } + + private: + absl::StatusOr EvaluateList(ExecutionFrameBase& frame, + const ListValue& list) const { + switch (op_) { + case StringSetListOp::kIntersects: { + bool intersects = false; + CEL_RETURN_IF_ERROR(list.ForEach( + [&](size_t /*index*/, + const Value& element) -> absl::StatusOr { + if (element.IsString() && set_.contains(element.GetString())) { + intersects = true; + return false; + } + return true; + }, + frame.descriptor_pool(), frame.message_factory(), frame.arena())); + return intersects; + } + case StringSetListOp::kContains: { + CEL_ASSIGN_OR_RETURN(size_t list_size, list.Size()); + if (list_size < set_.size()) { + return false; + } + absl::flat_hash_set matched; + CEL_RETURN_IF_ERROR(list.ForEach( + [&](size_t index, const Value& element) -> absl::StatusOr { + if (element.IsString()) { + auto it = set_.find(element.GetString()); + if (it != set_.end()) { + matched.insert(*it); + if (matched.size() == set_.size()) { + return false; + } + } + } + size_t remaining = index < list_size ? list_size - 1 - index : 0; + if (remaining < set_.size() - matched.size()) { + return false; + } + return true; + }, + frame.descriptor_pool(), frame.message_factory(), frame.arena())); + return matched.size() == set_.size(); + } + case StringSetListOp::kEquivalent: { + CEL_ASSIGN_OR_RETURN(size_t list_size, list.Size()); + if (list_size < set_.size()) { + return false; + } + bool all_in_set = true; + absl::flat_hash_set matched; + CEL_RETURN_IF_ERROR(list.ForEach( + [&](size_t /*index*/, + const Value& element) -> absl::StatusOr { + if (!element.IsString()) { + all_in_set = false; + return false; + } + auto it = set_.find(element.GetString()); + if (it == set_.end()) { + all_in_set = false; + return false; + } + matched.insert(*it); + return true; + }, + frame.descriptor_pool(), frame.message_factory(), frame.arena())); + return all_in_set && matched.size() == set_.size(); + } + } + return false; + } + + StringSetListOp op_; + StringSet set_; +}; + std::optional TryExtractBitmaskFromListExpr( const ListExpr& list_expr) { uint64_t bitmask = 0; @@ -145,6 +350,91 @@ std::optional TryExtractBitmask(PlannerContext& context, return std::nullopt; } +bool IsStringListType(const PlannerContext& context, int64_t expr_id) { + std::optional type = context.GetResolvedType(expr_id); + if (!type.has_value() || !type->IsList()) { + return false; + } + return type->GetList().GetElement().IsString(); +} + +std::optional TryExtractStringSetFromListExpr( + const ListExpr& list_expr, size_t min_set_size) { + if (list_expr.elements().empty() || + list_expr.elements().size() < min_set_size) { + return std::nullopt; + } + StringSet set; + set.reserve(list_expr.elements().size()); + for (const ListExprElement& element : list_expr.elements()) { + if (element.optional() || !element.has_expr() || + !element.expr().has_const_expr()) { + return std::nullopt; + } + const Constant& constant = element.expr().const_expr(); + if (constant.kind_case() != ConstantKindCase::kString) { + return std::nullopt; + } + set.insert(constant.string_value()); + } + return set; +} + +std::optional TryExtractStringSetFromFoldedList( + PlannerContext& context, const ListValue& list_value, size_t min_set_size) { + absl::StatusOr size = list_value.Size(); + if (!size.ok() || *size == 0 || *size < min_set_size) { + return std::nullopt; + } + StringSet set; + set.reserve(*size); + bool valid = true; + + absl::Status status = list_value.ForEach( + [&](size_t /*index*/, const Value& element) -> absl::StatusOr { + if (!element.IsString()) { + valid = false; + return false; + } + set.insert(element.GetString().ToString()); + return true; + }, + context.descriptor_pool(), context.MutableMessageFactory(), + context.MutableArena()); + + if (!status.ok() || !valid) { + return std::nullopt; + } + return set; +} + +std::optional TryExtractStringSet(PlannerContext& context, + const Expr& list_arg, + size_t min_set_size) { + if (!IsStringListType(context, list_arg.id())) { + return std::nullopt; + } + ExecutionPathView subplan = context.GetSubplan(list_arg); + if (subplan.empty()) { + return std::nullopt; + } + if (subplan.size() == 1) { + Value constant_value; + if (!GetIfConstant(subplan[0], constant_value) || + !constant_value.IsList()) { + return std::nullopt; + } + return TryExtractStringSetFromFoldedList(context, constant_value.GetList(), + min_set_size); + } + + if (list_arg.has_list_expr()) { + return TryExtractStringSetFromListExpr(list_arg.list_expr(), min_set_size); + } + + return std::nullopt; +} + bool IsInFunction(absl::string_view fn) { return fn == cel::builtin::kIn || fn == cel::builtin::kInDeprecated || fn == cel::builtin::kInFunction; @@ -203,12 +493,21 @@ class SetMembershipOptimizer : public ProgramOptimizer { return absl::OkStatus(); } std::optional bitmask = TryExtractBitmask(context, arg1); - if (!bitmask.has_value()) { - return absl::OkStatus(); + if (bitmask.has_value()) { + return ReplaceWithOptimizedStep( + context, node, arg0, + ExpressionStep::MakeBitIsSetStep(*bitmask, node.id())); + } + std::optional str_set = + TryExtractStringSet(context, arg1, options_.min_set_size); + if (str_set.has_value()) { + return ReplaceWithOptimizedStep( + context, node, arg0, + ExpressionStep::MakeGenericStep(std::make_unique( + std::move(*str_set), node.id()), + node.id(), /*stack_delta=*/0)); } - return ReplaceWithBitsetStep( - context, node, arg0, - ExpressionStep::MakeBitIsSetStep(*bitmask, node.id())); + return absl::OkStatus(); } if (fn == kSetsContains) { @@ -217,12 +516,22 @@ class SetMembershipOptimizer : public ProgramOptimizer { return absl::OkStatus(); } std::optional bitmask = TryExtractBitmask(context, arg1); - if (!bitmask.has_value()) { - return absl::OkStatus(); + if (bitmask.has_value()) { + return ReplaceWithOptimizedStep( + context, node, arg0, + ExpressionStep::MakeBitAndEqualStep(*bitmask, node.id())); } - return ReplaceWithBitsetStep( - context, node, arg0, - ExpressionStep::MakeBitAndEqualStep(*bitmask, node.id())); + std::optional str_set = + TryExtractStringSet(context, arg1, options_.min_set_size); + if (str_set.has_value()) { + return ReplaceWithOptimizedStep( + context, node, arg0, + ExpressionStep::MakeGenericStep( + std::make_unique( + StringSetListOp::kContains, std::move(*str_set), node.id()), + node.id(), /*stack_delta=*/0)); + } + return absl::OkStatus(); } if (fn == kSetsIntersects) { @@ -232,16 +541,35 @@ class SetMembershipOptimizer : public ProgramOptimizer { } std::optional bitmask = TryExtractBitmask(context, arg1); if (bitmask.has_value()) { - return ReplaceWithBitsetStep( + return ReplaceWithOptimizedStep( context, node, arg0, ExpressionStep::MakeBitAndIsNonZeroStep(*bitmask, node.id())); } bitmask = TryExtractBitmask(context, arg0); if (bitmask.has_value()) { - return ReplaceWithBitsetStep( + return ReplaceWithOptimizedStep( context, node, arg1, ExpressionStep::MakeBitAndIsNonZeroStep(*bitmask, node.id())); } + std::optional str_set = + TryExtractStringSet(context, arg1, options_.min_set_size); + if (str_set.has_value()) { + return ReplaceWithOptimizedStep( + context, node, arg0, + ExpressionStep::MakeGenericStep(std::make_unique( + StringSetListOp::kIntersects, + std::move(*str_set), node.id()), + node.id(), /*stack_delta=*/0)); + } + str_set = TryExtractStringSet(context, arg0, options_.min_set_size); + if (str_set.has_value()) { + return ReplaceWithOptimizedStep( + context, node, arg1, + ExpressionStep::MakeGenericStep(std::make_unique( + StringSetListOp::kIntersects, + std::move(*str_set), node.id()), + node.id(), /*stack_delta=*/0)); + } return absl::OkStatus(); } @@ -252,16 +580,35 @@ class SetMembershipOptimizer : public ProgramOptimizer { } std::optional bitmask = TryExtractBitmask(context, arg1); if (bitmask.has_value()) { - return ReplaceWithBitsetStep( + return ReplaceWithOptimizedStep( context, node, arg0, ExpressionStep::MakeBitEqualStep(*bitmask, node.id())); } bitmask = TryExtractBitmask(context, arg0); if (bitmask.has_value()) { - return ReplaceWithBitsetStep( + return ReplaceWithOptimizedStep( context, node, arg1, ExpressionStep::MakeBitEqualStep(*bitmask, node.id())); } + std::optional str_set = + TryExtractStringSet(context, arg1, options_.min_set_size); + if (str_set.has_value()) { + return ReplaceWithOptimizedStep( + context, node, arg0, + ExpressionStep::MakeGenericStep(std::make_unique( + StringSetListOp::kEquivalent, + std::move(*str_set), node.id()), + node.id(), /*stack_delta=*/0)); + } + str_set = TryExtractStringSet(context, arg0, options_.min_set_size); + if (str_set.has_value()) { + return ReplaceWithOptimizedStep( + context, node, arg1, + ExpressionStep::MakeGenericStep(std::make_unique( + StringSetListOp::kEquivalent, + std::move(*str_set), node.id()), + node.id(), /*stack_delta=*/0)); + } return absl::OkStatus(); } @@ -271,19 +618,19 @@ class SetMembershipOptimizer : public ProgramOptimizer { } private: - static absl::Status ReplaceWithBitsetStep(PlannerContext& context, - const Expr& call_node, - const Expr& var_arg, - ExpressionStep bitset_step) { + static absl::Status ReplaceWithOptimizedStep(PlannerContext& context, + const Expr& call_node, + const Expr& var_arg, + ExpressionStep step) { CEL_ASSIGN_OR_RETURN(ExecutionPath path, context.ExtractSubplan(var_arg)); - path.push_back(std::move(bitset_step)); + path.push_back(std::move(step)); return context.ReplaceSubplan(call_node, std::move(path)); } // TODO(b/572083203): expose this in the planner context instead of capturing // a reference. const Ast& ast_; - [[maybe_unused]] SetMembershipOptimizationOptions options_; + SetMembershipOptimizationOptions options_; }; FlatExprBuilder* GetFlatExprBuilder(RuntimeBuilder& builder) { @@ -307,6 +654,7 @@ ProgramOptimizerFactory CreateSetMembershipOptimizationProgramOptimizer( if (!context.options().enable_heterogeneous_equality) { return nullptr; } + context.set_resolve_types(); return std::make_unique(ast, options); }; } diff --git a/extensions/set_membership_optimization.h b/extensions/set_membership_optimization.h index 718b641e5..acf4db5a0 100644 --- a/extensions/set_membership_optimization.h +++ b/extensions/set_membership_optimization.h @@ -15,6 +15,8 @@ #ifndef THIRD_PARTY_CEL_CPP_EXTENSIONS_SET_MEMBERSHIP_OPTIMIZATION_H_ #define THIRD_PARTY_CEL_CPP_EXTENSIONS_SET_MEMBERSHIP_OPTIMIZATION_H_ +#include + #include "absl/status/status.h" #include "eval/compiler/flat_expr_builder_extensions.h" #include "runtime/runtime_builder.h" @@ -22,8 +24,12 @@ namespace cel::extensions { // Options for set membership optimization. -// TODO(b/572083203): Add options for non-bitset constant list optimizations. -struct SetMembershipOptimizationOptions {}; +struct SetMembershipOptimizationOptions { + // Minimum size of a non-bitset constant list (e.g. list) to optimize + // using a hash set. Optimistically assumes the input list elements are + // unique. + size_t min_set_size = 4; +}; // Enable set membership optimization on the given RuntimeBuilder. // @@ -32,10 +38,11 @@ struct SetMembershipOptimizationOptions {}; // This optimization assumes that `enable_heterogeneous_equality` is enabled. // // When enabled, the runtime will attempt to optimize branches that appear -// to implement a set membership tests against a constant list. For small +// to implement a set membership test against a constant list. For small // integer lists, this is implemented via a precomputed bitset. list is // emphasized to cover the common case of testing protobuf enums against a -// constant list. +// constant list. For constant list of size >= `options.min_set_size`, +// this is implemented via a precomputed hash set. // // Examples: // foo in [EnumA, EnumB, EnumC] diff --git a/extensions/set_membership_optimization_test.cc b/extensions/set_membership_optimization_test.cc index 1010a55e7..827108319 100644 --- a/extensions/set_membership_optimization_test.cc +++ b/extensions/set_membership_optimization_test.cc @@ -59,23 +59,30 @@ #include "runtime/standard_runtime_builder_factory.h" #include "cel/expr/conformance/proto2/test_all_types.pb.h" #include "google/protobuf/arena.h" -#include "google/protobuf/descriptor.h" namespace cel::extensions { namespace { using ::absl_testing::IsOk; -using ::cel::expr::conformance::proto2::TestAllTypes; using ::cel::runtime_internal::NewTestingRuntimeEnv; using ::cel::test::BoolValueIs; using ::cel::test::ValueKindIs; +using ::google::api::expr::runtime::ExpressionStepKind; using ::google::api::expr::runtime::FlatExprBuilder; using ::google::api::expr::runtime::FlatExpression; using ::google::api::expr::runtime::NewReferenceResolverExtension; using ::google::api::expr::runtime::ReferenceResolverOption; +using ::testing::ElementsAre; using ::testing::Not; using ::testing::SizeIs; +MATCHER_P(StepKindIs, expected_kind, "") { return arg.kind() == expected_kind; } + +struct BitsetPlanTestCase { + absl::string_view expr; + ExpressionStepKind expected_kind; +}; + absl::StatusOr> CreateTestCompiler() { CEL_ASSIGN_OR_RETURN( std::unique_ptr builder, @@ -90,6 +97,7 @@ absl::StatusOr> CreateTestCompiler() { CEL_RETURN_IF_ERROR(checker.AddVariable(MakeVariableDecl("x", IntType()))); CEL_RETURN_IF_ERROR(checker.AddVariable(MakeVariableDecl("u", UintType()))); CEL_RETURN_IF_ERROR(checker.AddVariable(MakeVariableDecl("d", DynType()))); + CEL_RETURN_IF_ERROR(checker.AddVariable(MakeVariableDecl("s", StringType()))); CEL_RETURN_IF_ERROR(checker.AddVariable( MakeVariableDecl("int_list", ListType(checker.arena(), IntType())))); CEL_RETURN_IF_ERROR(checker.AddVariable( @@ -176,24 +184,33 @@ TEST(SetMembershipOptimizationPlanTest, EmitsCompactBitsetPlan) { builder.AddProgramOptimizer( CreateSetMembershipOptimizationProgramOptimizer()); - for (absl::string_view expr_str : { - "x in [1, 2, 3, 4]", - "d in []", - "sets.contains(int_list, [1, 2, 3, 4])", - "sets.contains(str_list, [])", - "sets.intersects(int_list, [1, 2, 3, 4])", - "sets.intersects([1, 2, 3, 4], int_list)", - "sets.intersects(str_list, [])", - "sets.equivalent(int_list, [1, 2, 3, 4])", - "sets.equivalent([1, 2, 3, 4], int_list)", - "sets.equivalent(str_list, [])", - }) { - ASSERT_OK_AND_ASSIGN(std::unique_ptr ast, CompileTestExpr(expr_str)); + const BitsetPlanTestCase test_cases[] = { + {"x in [1, 2, 3, 4]", ExpressionStepKind::kBitIsSet}, + {"d in []", ExpressionStepKind::kBitIsSet}, + {"sets.contains(int_list, [1, 2, 3, 4])", + ExpressionStepKind::kBitAndEqual}, + {"sets.contains(str_list, [])", ExpressionStepKind::kBitAndEqual}, + {"sets.intersects(int_list, [1, 2, 3, 4])", + ExpressionStepKind::kBitAndIsNonZero}, + {"sets.intersects([1, 2, 3, 4], int_list)", + ExpressionStepKind::kBitAndIsNonZero}, + {"sets.intersects(str_list, [])", ExpressionStepKind::kBitAndIsNonZero}, + {"sets.equivalent(int_list, [1, 2, 3, 4])", + ExpressionStepKind::kBitEqual}, + {"sets.equivalent([1, 2, 3, 4], int_list)", + ExpressionStepKind::kBitEqual}, + {"sets.equivalent(str_list, [])", ExpressionStepKind::kBitEqual}, + }; + for (const BitsetPlanTestCase& tc : test_cases) { + ASSERT_OK_AND_ASSIGN(std::unique_ptr ast, CompileTestExpr(tc.expr)); ASSERT_OK_AND_ASSIGN(FlatExpression plan, builder.CreateExpressionImpl(std::move(ast), nullptr)); // Optimized plan consists of just 2 steps: reading the variable and // evaluating the bitset step. - EXPECT_THAT(plan.path(), SizeIs(2)) << expr_str; + EXPECT_THAT(plan.path(), + ElementsAre(StepKindIs(ExpressionStepKind::kIdentifier), + StepKindIs(tc.expected_kind))) + << tc.expr; } } @@ -216,48 +233,202 @@ TEST(SetMembershipOptimizationPlanTest, builder.AddProgramOptimizer( CreateSetMembershipOptimizationProgramOptimizer()); + const BitsetPlanTestCase test_cases[] = { + {"x in ([1, 2] + [3, 4])", ExpressionStepKind::kBitIsSet}, + {"sets.contains(int_list, [1, 2] + [3, 4])", + ExpressionStepKind::kBitAndEqual}, + {"sets.intersects(int_list, [1, 2] + [3, 4])", + ExpressionStepKind::kBitAndIsNonZero}, + {"sets.equivalent([1, 2] + [3, 4], int_list)", + ExpressionStepKind::kBitEqual}, + }; + for (const BitsetPlanTestCase& tc : test_cases) { + ASSERT_OK_AND_ASSIGN(std::unique_ptr ast, CompileTestExpr(tc.expr)); + ASSERT_OK_AND_ASSIGN(FlatExpression plan, + builder.CreateExpressionImpl(std::move(ast), nullptr)); + // Constant folding folds `[1, 2] + [3, 4]` into a single constant ListValue + // step, which SetMembershipOptimization then replaces with a 2-step plan. + EXPECT_THAT(plan.path(), + ElementsAre(StepKindIs(ExpressionStepKind::kIdentifier), + StepKindIs(tc.expected_kind))) + << tc.expr; + } +} + +TEST(SetMembershipOptimizationPlanTest, EmitsCompactStringSetPlan) { + RuntimeOptions options; + options.container = "cel.expr.conformance.proto2"; + options.enable_heterogeneous_equality = true; + std::shared_ptr env = + NewTestingRuntimeEnv(); + ASSERT_THAT(RegisterStandardFunctions(env->function_registry, options), + IsOk()); + ASSERT_THAT(RegisterSetsFunctions(env->function_registry, options), IsOk()); + FlatExprBuilder builder(std::move(env), options); + builder.set_container(options.container); + builder.AddAstTransform( + NewReferenceResolverExtension(ReferenceResolverOption::kAlways)); + builder.AddProgramOptimizer( + CreateSetMembershipOptimizationProgramOptimizer()); + for (absl::string_view expr_str : { - "x in ([1, 2] + [3, 4])", - "sets.contains(int_list, [1, 2] + [3, 4])", - "sets.intersects(int_list, [1, 2] + [3, 4])", - "sets.equivalent([1, 2] + [3, 4], int_list)", + "s in ['a', 'b', 'c', 'd']", + "d in ['a', 'b', 'c', 'd']", + "sets.contains(str_list, ['a', 'b', 'c', 'd'])", + "sets.intersects(str_list, ['a', 'b', 'c', 'd'])", + "sets.intersects(['a', 'b', 'c', 'd'], str_list)", + "sets.equivalent(str_list, ['a', 'b', 'c', 'd'])", + "sets.equivalent(['a', 'b', 'c', 'd'], str_list)", }) { ASSERT_OK_AND_ASSIGN(std::unique_ptr ast, CompileTestExpr(expr_str)); ASSERT_OK_AND_ASSIGN(FlatExpression plan, builder.CreateExpressionImpl(std::move(ast), nullptr)); - // Constant folding folds `[1, 2] + [3, 4]` into a single constant ListValue - // step, which SetMembershipOptimization then replaces with a 2-step plan. - EXPECT_THAT(plan.path(), SizeIs(2)) << expr_str; + // Optimized plan consists of just 2 steps: reading the variable and + // evaluating the generic string set step. + EXPECT_THAT(plan.path(), + ElementsAre(StepKindIs(ExpressionStepKind::kIdentifier), + StepKindIs(ExpressionStepKind::kGenericLogic))) + << expr_str; + } + + // Constant string lists smaller than default min_set_size (4) are not + // optimized. + for (absl::string_view expr_str : { + "s in ['a', 'b', 'c']", + "sets.contains(str_list, ['a', 'b', 'c'])", + "sets.intersects(str_list, ['a', 'b', 'c'])", + "sets.equivalent(str_list, ['a', 'b', 'c'])", + }) { + ASSERT_OK_AND_ASSIGN(std::unique_ptr ast, CompileTestExpr(expr_str)); + ASSERT_OK_AND_ASSIGN(FlatExpression plan, + builder.CreateExpressionImpl(std::move(ast), nullptr)); + EXPECT_THAT(plan.path(), Not(SizeIs(2))) << expr_str; + } + + // Constant list containing strings is not optimized as a string set. + for ( + absl::string_view expr_str : { + "s in [dyn('a'), dyn('b'), dyn('c'), dyn('d')]", + "sets.contains(str_list, [dyn('a'), dyn('b'), dyn('c'), dyn('d')])", + "sets.intersects(str_list, [dyn('a'), dyn('b'), dyn('c'), dyn('d')])", + "sets.equivalent(str_list, [dyn('a'), dyn('b'), dyn('c'), dyn('d')])", + }) { + ASSERT_OK_AND_ASSIGN(std::unique_ptr ast, CompileTestExpr(expr_str)); + ASSERT_OK_AND_ASSIGN(FlatExpression plan, + builder.CreateExpressionImpl(std::move(ast), nullptr)); + EXPECT_THAT(plan.path(), Not(SizeIs(2))) << expr_str; } } -TEST(SetMembershipOptimizationPlanTest, EmitsCompactBitsetPlanForUncheckedAst) { +TEST(SetMembershipOptimizationPlanTest, + RespectsCustomMinSetSizeForConstantStringSets) { + RuntimeOptions options; + options.container = "cel.expr.conformance.proto2"; + options.enable_heterogeneous_equality = true; + std::shared_ptr env = + NewTestingRuntimeEnv(); + ASSERT_THAT(RegisterSetsFunctions(env->function_registry, options), IsOk()); + FlatExprBuilder builder(std::move(env), options); + builder.set_container(options.container); + builder.AddAstTransform( + NewReferenceResolverExtension(ReferenceResolverOption::kAlways)); + builder.AddProgramOptimizer(CreateSetMembershipOptimizationProgramOptimizer( + SetMembershipOptimizationOptions{.min_set_size = 2})); + + for (absl::string_view expr_str : { + "s in ['a', 'b']", + "sets.contains(str_list, ['a', 'b'])", + "sets.intersects(str_list, ['a', 'b'])", + "sets.equivalent(str_list, ['a', 'b'])", + }) { + ASSERT_OK_AND_ASSIGN(std::unique_ptr ast, CompileTestExpr(expr_str)); + ASSERT_OK_AND_ASSIGN(FlatExpression plan, + builder.CreateExpressionImpl(std::move(ast), nullptr)); + EXPECT_THAT(plan.path(), + ElementsAre(StepKindIs(ExpressionStepKind::kIdentifier), + StepKindIs(ExpressionStepKind::kGenericLogic))) + << expr_str; + } + + // Size 1 is still below min_set_size = 2. + { + ASSERT_OK_AND_ASSIGN(std::unique_ptr ast, + CompileTestExpr("s in ['a']")); + ASSERT_OK_AND_ASSIGN(FlatExpression plan, + builder.CreateExpressionImpl(std::move(ast), nullptr)); + EXPECT_THAT(plan.path(), Not(SizeIs(2))); + } +} + +TEST(SetMembershipOptimizationPlanTest, + EmitsCompactStringSetPlanWithConstantFoldedList) { RuntimeOptions options; options.container = "cel.expr.conformance.proto2"; options.enable_heterogeneous_equality = true; std::shared_ptr env = NewTestingRuntimeEnv(); + ASSERT_THAT(RegisterStandardFunctions(env->function_registry, options), + IsOk()); ASSERT_THAT(RegisterSetsFunctions(env->function_registry, options), IsOk()); FlatExprBuilder builder(std::move(env), options); builder.set_container(options.container); builder.AddAstTransform( NewReferenceResolverExtension(ReferenceResolverOption::kAlways)); + builder.AddProgramOptimizer( + cel::runtime_internal::CreateConstantFoldingOptimizer()); builder.AddProgramOptimizer( CreateSetMembershipOptimizationProgramOptimizer()); for (absl::string_view expr_str : { - "x in [1, 2, 3, 4]", - "sets.contains(int_list, [1, 2, 3, 4])", - "sets.intersects(int_list, [1, 2, 3, 4])", - "sets.equivalent(int_list, [1, 2, 3, 4])", + "s in (['a', 'b'] + ['c', 'd'])", + "sets.contains(str_list, ['a', 'b'] + ['c', 'd'])", + "sets.intersects(str_list, ['a', 'b'] + ['c', 'd'])", + "sets.equivalent(['a', 'b'] + ['c', 'd'], str_list)", }) { ASSERT_OK_AND_ASSIGN(std::unique_ptr ast, CompileTestExpr(expr_str)); + ASSERT_OK_AND_ASSIGN(FlatExpression plan, + builder.CreateExpressionImpl(std::move(ast), nullptr)); + EXPECT_THAT(plan.path(), + ElementsAre(StepKindIs(ExpressionStepKind::kIdentifier), + StepKindIs(ExpressionStepKind::kGenericLogic))) + << expr_str; + } +} + +TEST(SetMembershipOptimizationPlanTest, EmitsCompactBitsetPlanForUncheckedAst) { + RuntimeOptions options; + options.container = "cel.expr.conformance.proto2"; + options.enable_heterogeneous_equality = true; + std::shared_ptr env = + NewTestingRuntimeEnv(); + ASSERT_THAT(RegisterSetsFunctions(env->function_registry, options), IsOk()); + FlatExprBuilder builder(std::move(env), options); + builder.set_container(options.container); + builder.AddAstTransform( + NewReferenceResolverExtension(ReferenceResolverOption::kAlways)); + builder.AddProgramOptimizer( + CreateSetMembershipOptimizationProgramOptimizer()); + + const BitsetPlanTestCase test_cases[] = { + {"x in [1, 2, 3, 4]", ExpressionStepKind::kBitIsSet}, + {"sets.contains(int_list, [1, 2, 3, 4])", + ExpressionStepKind::kBitAndEqual}, + {"sets.intersects(int_list, [1, 2, 3, 4])", + ExpressionStepKind::kBitAndIsNonZero}, + {"sets.equivalent(int_list, [1, 2, 3, 4])", + ExpressionStepKind::kBitEqual}, + }; + for (const BitsetPlanTestCase& tc : test_cases) { + ASSERT_OK_AND_ASSIGN(std::unique_ptr ast, CompileTestExpr(tc.expr)); ast->mutable_reference_map().clear(); ast->mutable_type_map().clear(); ast->set_is_checked(false); ASSERT_OK_AND_ASSIGN(FlatExpression plan, builder.CreateExpressionImpl(std::move(ast), nullptr)); - EXPECT_THAT(plan.path(), SizeIs(2)) << expr_str; + EXPECT_THAT(plan.path(), + ElementsAre(StepKindIs(ExpressionStepKind::kIdentifier), + StepKindIs(tc.expected_kind))) + << tc.expr; } } @@ -403,13 +574,16 @@ TEST_P(SetMembershipOptimizationEvalTest, EvaluatesCorrectly) { activation.InsertOrAssignValue("x", IntValue(2)); activation.InsertOrAssignValue("u", UintValue(63)); activation.InsertOrAssignValue("d", DoubleValue(2.0)); + activation.InsertOrAssignValue("s", StringValue::From("b", &arena)); activation.InsertOrAssignValue("int_list", MakeIntList(&arena, {0, 2, 63})); activation.InsertOrAssignValue("uint_list", MakeUintList(&arena, {0, 2})); activation.InsertOrAssignValue( "dyn_list", MakeValueList(&arena, {IntValue(0), DoubleValue(2.0)})); activation.InsertOrAssignValue( "str_list", MakeValueList(&arena, {StringValue::From("a", &arena), - StringValue::From("b", &arena)})); + StringValue::From("b", &arena), + StringValue::From("c", &arena), + StringValue::From("d", &arena)})); const std::vector test_cases = { // @in with int, uint, dyn(double), and protobuf enums @@ -480,17 +654,46 @@ TEST_P(SetMembershipOptimizationEvalTest, EvaluatesCorrectly) { {"sets.equivalent(int_list.filter(i, i < 0), [])", true}, {"sets.equivalent(str_list.filter(s, s == 'none'), [])", true}, + // Constant string sets (size >= 4) + {"s in ['a', 'b', 'c', 'd']", true}, + {"s in ['a', 'c', 'd', 'e']", false}, + {"d in ['a', 'b', 'c', 'd']", false}, + {"sets.contains(str_list, ['a', 'b', 'c', 'd'])", true}, + {"sets.contains(str_list, ['a', 'b', 'b', 'c'])", true}, + {"sets.contains(str_list, ['a', 'b', 'c', 'e'])", false}, + {"sets.contains(['a', 'b', 'c'], ['a', 'b', 'c', 'd'])", false}, + {"sets.contains([dyn('a'), dyn('b'), dyn('c'), dyn('d'), dyn(1)], " + "['a', 'b', 'c', 'd'])", + true}, + {"sets.intersects(str_list, ['b', 'e', 'f', 'g'])", true}, + {"sets.intersects(['b', 'e', 'f', 'g'], str_list)", true}, + {"sets.intersects(str_list, ['e', 'f', 'g', 'h'])", false}, + {"sets.intersects(['e', 'f', 'g', 'h'], str_list)", false}, + {"sets.intersects([dyn(1), dyn('d')], ['a', 'b', 'c', 'd'])", true}, + {"sets.equivalent(str_list, ['d', 'c', 'b', 'a'])", true}, + {"sets.equivalent(['d', 'c', 'b', 'a'], str_list)", true}, + {"sets.equivalent(str_list, ['a', 'b', 'b', 'c', 'd'])", true}, + {"sets.equivalent(['a', 'a', 'b', 'c', 'd'], " + "['a', 'b', 'c', 'd'])", + true}, + {"sets.equivalent(str_list, ['a', 'b', 'c', 'e'])", false}, + {"sets.equivalent(['a', 'b', 'c', 'e'], str_list)", false}, + {"sets.equivalent(['a', 'b', 'c'], ['a', 'b', 'c', 'd'])", false}, + {"sets.equivalent([dyn('a'), dyn('b'), dyn('c'), dyn('d'), dyn(1)], " + "['a', 'b', 'c', 'd'])", + false}, + // Fallback when constant list has out-of-range values, mixed types, or - // strings + // small string lists (< 4 elements) {"x in [-1, 2, 100]", true}, {"100 in [-1, 2, 100]", true}, {"-1 in [-1, 2, 100]", true}, {"x in [1, 64]", false}, {"d in [dyn(1), dyn(2u), dyn(3.5)]", true}, - {"'a' in ['a', 'b', 'c', 'd']", true}, + {"'a' in ['a', 'b', 'c']", true}, {"sets.contains(str_list, ['a', 'b'])", true}, - {"sets.intersects(str_list, ['b', 'c'])", true}, - {"sets.equivalent(str_list, ['b', 'a'])", true}, + {"sets.intersects(str_list, ['b', 'e'])", true}, + {"sets.equivalent(['a', 'b'], ['b', 'a'])", true}, // Heterogeneous non-numeric dynamic elements with constant int sets {"dyn('1') in [0, 1, 2]", false}, @@ -501,6 +704,10 @@ TEST_P(SetMembershipOptimizationEvalTest, EvaluatesCorrectly) { // Constant-folded list expression when constant folding is enabled {"x in ([0, 1] + [2, 3])", true}, {"sets.contains(int_list, [0] + [2])", true}, + {"s in (['a', 'b'] + ['c', 'd'])", true}, + {"sets.contains(str_list, ['a', 'b'] + ['c', 'd'])", true}, + {"sets.intersects(str_list, ['e', 'f'] + ['g', 'd'])", true}, + {"sets.equivalent(str_list, ['d', 'c'] + ['b', 'a'])", true}, }; for (const EvalTestCase& tc : test_cases) { @@ -524,15 +731,21 @@ TEST(SetMembershipOptimizationUnknownAndErrorTest, google::protobuf::Arena arena; Activation activation; - ASSERT_THAT(activation.SetUnknownPatterns({AttributePattern("x", {}), - AttributePattern("int_list", {})}), - IsOk()); + ASSERT_THAT( + activation.SetUnknownPatterns( + {AttributePattern("x", {}), AttributePattern("s", {}), + AttributePattern("int_list", {}), AttributePattern("str_list", {})}), + IsOk()); for (absl::string_view expr_str : { "x in [1, 2, 3]", "sets.contains(int_list, [1, 2])", "sets.intersects(int_list, [1, 2])", "sets.equivalent(int_list, [1, 2])", + "s in ['a', 'b', 'c', 'd']", + "sets.contains(str_list, ['a', 'b', 'c', 'd'])", + "sets.intersects(str_list, ['a', 'b', 'c', 'd'])", + "sets.equivalent(str_list, ['a', 'b', 'c', 'd'])", }) { ASSERT_OK_AND_ASSIGN(std::unique_ptr ast, CompileTestExpr(expr_str)); ASSERT_OK_AND_ASSIGN(std::unique_ptr program, @@ -542,13 +755,18 @@ TEST(SetMembershipOptimizationUnknownAndErrorTest, } // Runtime error in operand propagates as ErrorValue. - { - ASSERT_OK_AND_ASSIGN(std::unique_ptr ast, - CompileTestExpr("(1 / 0) in [1, 2, 3]")); + for (absl::string_view expr_str : { + "(1 / 0) in [1, 2, 3]", + "dyn(1 / 0) in ['a', 'b', 'c', 'd']", + "sets.contains(dyn(1 / 0), ['a', 'b', 'c', 'd'])", + "sets.intersects(dyn(1 / 0), ['a', 'b', 'c', 'd'])", + "sets.equivalent(dyn(1 / 0), ['a', 'b', 'c', 'd'])", + }) { + ASSERT_OK_AND_ASSIGN(std::unique_ptr ast, CompileTestExpr(expr_str)); ASSERT_OK_AND_ASSIGN(std::unique_ptr program, runtime->CreateProgram(std::move(ast))); ASSERT_OK_AND_ASSIGN(Value result, program->Evaluate(&arena, activation)); - EXPECT_THAT(result, ValueKindIs(ValueKind::kError)); + EXPECT_THAT(result, ValueKindIs(ValueKind::kError)) << expr_str; } } diff --git a/extensions/sets_functions_benchmark_test.cc b/extensions/sets_functions_benchmark_test.cc index 493a46d2b..acea32381 100644 --- a/extensions/sets_functions_benchmark_test.cc +++ b/extensions/sets_functions_benchmark_test.cc @@ -194,6 +194,12 @@ absl::StatusOr> CreateBenchmarkCompiler() { MakeVariableDecl("x", ListType(checker.arena(), IntType())))); CEL_RETURN_IF_ERROR(checker.AddVariable( MakeVariableDecl("y", ListType(checker.arena(), IntType())))); + CEL_RETURN_IF_ERROR(checker.AddVariable( + MakeVariableDecl("str_x", ListType(checker.arena(), StringType())))); + CEL_RETURN_IF_ERROR(checker.AddVariable( + MakeVariableDecl("str_y", ListType(checker.arena(), StringType())))); + CEL_RETURN_IF_ERROR( + checker.AddVariable(MakeVariableDecl("str_elem", StringType()))); return builder->Build(); } @@ -388,5 +394,204 @@ BENCHMARK(BM_SetsEquivalentComprehensionFalse)->Apply(BenchArgs); BENCHMARK(BM_SetsEquivalentTrue)->Apply(BenchArgs); BENCHMARK(BM_SetsEquivalentFalse)->Apply(BenchArgs); +enum class StringSetBenchOp { + kIn, + kIntersects, + kContains, + kEquivalent, +}; + +std::string ConstantStringList(int len) { + std::vector elems; + elems.reserve(len); + for (int i = 0; i < len; ++i) { + elems.push_back(absl::StrCat("'str_", i, "'")); + } + return absl::StrCat("[", absl::StrJoin(elems, ", "), "]"); +} + +absl::Status RegisterStringBenchValues(StringSetBenchOp op, bool expected, + int len, + google::protobuf::Arena* absl_nonnull arena, + Activation& activation) { + ListValueBuilderPtr y_builder = cel::NewListValueBuilder(arena); + y_builder->Reserve(len); + for (int i = 0; i < len; ++i) { + CEL_RETURN_IF_ERROR( + y_builder->Add(cel::StringValue::From(absl::StrCat("str_", i), arena))); + } + activation.InsertOrAssignValue("str_y", std::move(*y_builder).Build()); + + if (op == StringSetBenchOp::kIn) { + // True: best case / short-circuit (matches first element). + // False: worst case (scans all elements). + std::string elem = expected ? "str_0" : "str_missing"; + activation.InsertOrAssignValue("str_elem", + cel::StringValue::From(elem, arena)); + return absl::OkStatus(); + } + + ListValueBuilderPtr x_builder = cel::NewListValueBuilder(arena); + x_builder->Reserve(len); + if (op == StringSetBenchOp::kIntersects) { + // True: best case / short-circuit (matches on the first element). + // False: worst case (disjoint lists, scans all elements). + std::string first = expected ? "str_0" : "other_0"; + CEL_RETURN_IF_ERROR(x_builder->Add(cel::StringValue::From(first, arena))); + for (int i = 1; i < len; ++i) { + CEL_RETURN_IF_ERROR(x_builder->Add( + cel::StringValue::From(absl::StrCat("other_", i), arena))); + } + } else { + // For contains and equivalent: + // True: worst case (str_x has all elements in reverse order, exercising the + // full loop and matched set allocation). + // False: best case / short-circuit (disjoint elements starting with + // "other_0", short-circuiting on the first element for equivalent and + // avoiding matched set allocation for contains). + if (expected) { + for (int i = len - 1; i >= 0; --i) { + CEL_RETURN_IF_ERROR(x_builder->Add( + cel::StringValue::From(absl::StrCat("str_", i), arena))); + } + } else { + for (int i = 0; i < len; ++i) { + CEL_RETURN_IF_ERROR(x_builder->Add( + cel::StringValue::From(absl::StrCat("other_", i), arena))); + } + } + } + activation.InsertOrAssignValue("str_x", std::move(*x_builder).Build()); + return absl::OkStatus(); +} + +void RunStringSetBenchmark(StringSetBenchOp op, const TestCase& test_case, + benchmark::State& state) { + std::string expr = test_case.expr; + if (test_case.list_impl == ListImpl::kRhsConstant || + test_case.list_impl == ListImpl::kRhsConstantOptimized) { + expr = absl::StrReplaceAll(expr, + {{"str_y", ConstantStringList(test_case.size)}}); + } + + ASSERT_OK_AND_ASSIGN(std::unique_ptr compiler, + CreateBenchmarkCompiler()); + ASSERT_OK_AND_ASSIGN(ValidationResult validation_result, + compiler->Compile(expr)); + ASSERT_TRUE(validation_result.IsValid()) << validation_result.FormatError(); + ASSERT_OK_AND_ASSIGN(std::unique_ptr ast, + validation_result.ReleaseAst()); + + google::protobuf::Arena arena; + ASSERT_OK_AND_ASSIGN( + std::unique_ptr runtime, + CreateBenchmarkRuntime( + test_case.list_impl == ListImpl::kRhsConstantOptimized, &arena)); + ASSERT_OK_AND_ASSIGN(std::unique_ptr program, + runtime->CreateProgram(std::move(ast))); + + Activation activation; + ASSERT_THAT(RegisterStringBenchValues(op, test_case.result, test_case.size, + &arena, activation), + IsOk()); + + state.SetLabel(test_case.MakeLabel(test_case.size)); + for (auto _ : state) { + ASSERT_OK_AND_ASSIGN(Value result, program->Evaluate(&arena, activation)); + ASSERT_TRUE(result.IsBool()); + ASSERT_EQ(result.GetBool().NativeValue(), test_case.result) + << test_case.test_name; + } +} + +void BM_StringSetsInTrue(benchmark::State& state) { + ListImpl impl = FromNumber(state.range(0)); + int size = state.range(1); + RunStringSetBenchmark(StringSetBenchOp::kIn, + {"str_in_true", "str_elem in str_y", impl, size, true}, + state); +} + +void BM_StringSetsInFalse(benchmark::State& state) { + ListImpl impl = FromNumber(state.range(0)); + int size = state.range(1); + RunStringSetBenchmark( + StringSetBenchOp::kIn, + {"str_in_false", "str_elem in str_y", impl, size, false}, state); +} + +void BM_StringSetsIntersectsTrue(benchmark::State& state) { + ListImpl impl = FromNumber(state.range(0)); + int size = state.range(1); + RunStringSetBenchmark(StringSetBenchOp::kIntersects, + {"str_sets.intersects_true", + "sets.intersects(str_x, str_y)", impl, size, true}, + state); +} + +void BM_StringSetsIntersectsFalse(benchmark::State& state) { + ListImpl impl = FromNumber(state.range(0)); + int size = state.range(1); + RunStringSetBenchmark(StringSetBenchOp::kIntersects, + {"str_sets.intersects_false", + "sets.intersects(str_x, str_y)", impl, size, false}, + state); +} + +void BM_StringSetsContainsTrue(benchmark::State& state) { + ListImpl impl = FromNumber(state.range(0)); + int size = state.range(1); + RunStringSetBenchmark(StringSetBenchOp::kContains, + {"str_sets.contains_true", + "sets.contains(str_x, str_y)", impl, size, true}, + state); +} + +void BM_StringSetsContainsFalse(benchmark::State& state) { + ListImpl impl = FromNumber(state.range(0)); + int size = state.range(1); + RunStringSetBenchmark(StringSetBenchOp::kContains, + {"str_sets.contains_false", + "sets.contains(str_x, str_y)", impl, size, false}, + state); +} + +void BM_StringSetsEquivalentTrue(benchmark::State& state) { + ListImpl impl = FromNumber(state.range(0)); + int size = state.range(1); + RunStringSetBenchmark(StringSetBenchOp::kEquivalent, + {"str_sets.equivalent_true", + "sets.equivalent(str_x, str_y)", impl, size, true}, + state); +} + +void BM_StringSetsEquivalentFalse(benchmark::State& state) { + ListImpl impl = FromNumber(state.range(0)); + int size = state.range(1); + RunStringSetBenchmark(StringSetBenchOp::kEquivalent, + {"str_sets.equivalent_false", + "sets.equivalent(str_x, str_y)", impl, size, false}, + state); +} + +template +void StringBenchArgs(Benchmark* bench) { + for (ListImpl impl : + {ListImpl::kRhsConstant, ListImpl::kRhsConstantOptimized}) { + for (int size : {1, 4, 8, 16, 32, 64, 256}) { + bench->ArgPair(ToNumber(impl), size); + } + } +} + +BENCHMARK(BM_StringSetsInTrue)->Apply(StringBenchArgs); +BENCHMARK(BM_StringSetsInFalse)->Apply(StringBenchArgs); +BENCHMARK(BM_StringSetsIntersectsTrue)->Apply(StringBenchArgs); +BENCHMARK(BM_StringSetsIntersectsFalse)->Apply(StringBenchArgs); +BENCHMARK(BM_StringSetsContainsTrue)->Apply(StringBenchArgs); +BENCHMARK(BM_StringSetsContainsFalse)->Apply(StringBenchArgs); +BENCHMARK(BM_StringSetsEquivalentTrue)->Apply(StringBenchArgs); +BENCHMARK(BM_StringSetsEquivalentFalse)->Apply(StringBenchArgs); + } // namespace } // namespace cel::extensions