From 03c487c9bc077a4740e70d1bbecde44eeb4eccba Mon Sep 17 00:00:00 2001 From: Ishaan Maheshwari Date: Sun, 11 Oct 2026 05:34:50 +0530 Subject: [PATCH] GH-53159: [C++][Compute] Cache substring and regex matchers across invocations in scalar string kernels --- .../compute/kernels/scalar_string_ascii.cc | 240 ++++++++++++------ 1 file changed, 156 insertions(+), 84 deletions(-) diff --git a/cpp/src/arrow/compute/kernels/scalar_string_ascii.cc b/cpp/src/arrow/compute/kernels/scalar_string_ascii.cc index 8e7b626837fe..e1b769a2850c 100644 --- a/cpp/src/arrow/compute/kernels/scalar_string_ascii.cc +++ b/cpp/src/arrow/compute/kernels/scalar_string_ascii.cc @@ -1216,8 +1216,6 @@ void StringBoolTransform(KernelContext* ctx, const ExecSpan& batch, } } -using MatchSubstringState = OptionsWrapper; - // This is an implementation of the Knuth-Morris-Pratt algorithm struct PlainSubstringMatcher { const MatchSubstringOptions& options_; @@ -1308,7 +1306,7 @@ struct PlainEndsWithMatcher { #ifdef ARROW_WITH_RE2 struct RegexSubstringMatcher { - const MatchSubstringOptions& options_; + MatchSubstringOptions options_; const RE2 regex_match_; static Result> Make( @@ -1318,11 +1316,11 @@ struct RegexSubstringMatcher { return matcher; } - explicit RegexSubstringMatcher(const MatchSubstringOptions& options, - bool is_utf8 = true, bool literal = false) - : options_(options), + explicit RegexSubstringMatcher(MatchSubstringOptions options, bool is_utf8 = true, + bool literal = false) + : options_(std::move(options)), regex_match_(options_.pattern, - MakeRE2Options(is_utf8, options.ignore_case, literal)) {} + MakeRE2Options(is_utf8, options_.ignore_case, literal)) {} bool Match(std::string_view current) const { auto piece = re2::StringPiece(current.data(), current.length()); @@ -1331,6 +1329,46 @@ struct RegexSubstringMatcher { }; #endif +struct MatchSubstringState : public KernelState { + MatchSubstringOptions options; + + std::unique_ptr plain_substring_matcher; + std::unique_ptr plain_starts_with_matcher; + std::unique_ptr plain_ends_with_matcher; +#ifdef ARROW_WITH_RE2 + std::unique_ptr regex_substring_matcher; + + enum class LikeKind { kNone, kSubstring, kStartsWith, kEndsWith, kRegex }; + LikeKind like_kind = LikeKind::kNone; + std::unique_ptr like_state; +#endif + + explicit MatchSubstringState(MatchSubstringOptions options) + : options(std::move(options)) {} + + static Result> Init(KernelContext* ctx, + const KernelInitArgs& args) { + if (auto options = static_cast(args.options)) { + return std::make_unique(*options); + } + + return Status::Invalid( + "Attempted to initialize KernelState from null FunctionOptions"); + } + + static const MatchSubstringOptions& Get(const KernelState& state) { + return ::arrow::internal::checked_cast(state).options; + } + + static const MatchSubstringOptions& Get(KernelContext* ctx) { + return Get(*ctx->state()); + } + + static MatchSubstringState* GetState(KernelContext* ctx) { + return ::arrow::internal::checked_cast(ctx->state()); + } +}; + template struct MatchSubstringImpl { using offset_type = typename Type::offset_type; @@ -1361,7 +1399,6 @@ struct MatchSubstringImpl { template struct MatchSubstring { static Status Exec(KernelContext* ctx, const ExecSpan& batch, ExecResult* out) { - // TODO Cache matcher across invocations (for regex compilation) ARROW_ASSIGN_OR_RAISE(auto matcher, Matcher::Make(MatchSubstringState::Get(ctx))); return MatchSubstringImpl::Exec(ctx, batch, out, matcher.get()); } @@ -1371,12 +1408,14 @@ struct MatchSubstring { template struct MatchSubstring { static Status Exec(KernelContext* ctx, const ExecSpan& batch, ExecResult* out) { - // TODO Cache matcher across invocations (for regex compilation) - const bool is_utf8 = is_string_or_string_view(batch[0].type()->id()); - ARROW_ASSIGN_OR_RAISE(auto matcher, RegexSubstringMatcher::Make( - MatchSubstringState::Get(ctx), is_utf8)); - return MatchSubstringImpl::Exec(ctx, batch, out, - matcher.get()); + auto* state = MatchSubstringState::GetState(ctx); + if (!state->regex_substring_matcher) { + const bool is_utf8 = is_string_or_string_view(batch[0].type()->id()); + ARROW_ASSIGN_OR_RAISE(state->regex_substring_matcher, + RegexSubstringMatcher::Make(state->options, is_utf8)); + } + return MatchSubstringImpl::Exec( + ctx, batch, out, state->regex_substring_matcher.get()); } }; #endif @@ -1384,67 +1423,83 @@ struct MatchSubstring { template struct MatchSubstring { static Status Exec(KernelContext* ctx, const ExecSpan& batch, ExecResult* out) { - auto options = MatchSubstringState::Get(ctx); - if (options.ignore_case) { + auto* state = MatchSubstringState::GetState(ctx); + if (state->options.ignore_case) { #ifdef ARROW_WITH_RE2 - const bool is_utf8 = is_string_or_string_view(batch[0].type()->id()); - ARROW_ASSIGN_OR_RAISE(auto matcher, RegexSubstringMatcher::Make(options, is_utf8, - /*literal=*/true)); - return MatchSubstringImpl::Exec(ctx, batch, out, - matcher.get()); + if (!state->regex_substring_matcher) { + const bool is_utf8 = is_string_or_string_view(batch[0].type()->id()); + ARROW_ASSIGN_OR_RAISE( + state->regex_substring_matcher, + RegexSubstringMatcher::Make(state->options, is_utf8, /*literal=*/true)); + } + return MatchSubstringImpl::Exec( + ctx, batch, out, state->regex_substring_matcher.get()); #else return Status::NotImplemented("ignore_case requires RE2"); #endif } - ARROW_ASSIGN_OR_RAISE(auto matcher, PlainSubstringMatcher::Make(options)); - return MatchSubstringImpl::Exec(ctx, batch, out, - matcher.get()); + if (!state->plain_substring_matcher) { + ARROW_ASSIGN_OR_RAISE(state->plain_substring_matcher, + PlainSubstringMatcher::Make(state->options)); + } + return MatchSubstringImpl::Exec( + ctx, batch, out, state->plain_substring_matcher.get()); } }; template struct MatchSubstring { static Status Exec(KernelContext* ctx, const ExecSpan& batch, ExecResult* out) { - auto options = MatchSubstringState::Get(ctx); - if (options.ignore_case) { + auto* state = MatchSubstringState::GetState(ctx); + if (state->options.ignore_case) { #ifdef ARROW_WITH_RE2 - MatchSubstringOptions converted_options = options; - converted_options.pattern = "^" + RE2::QuoteMeta(options.pattern); - const bool is_utf8 = is_string_or_string_view(batch[0].type()->id()); - ARROW_ASSIGN_OR_RAISE(auto matcher, - RegexSubstringMatcher::Make(converted_options, is_utf8)); - return MatchSubstringImpl::Exec(ctx, batch, out, - matcher.get()); + if (!state->regex_substring_matcher) { + MatchSubstringOptions converted_options = state->options; + converted_options.pattern = "^" + RE2::QuoteMeta(state->options.pattern); + const bool is_utf8 = is_string_or_string_view(batch[0].type()->id()); + ARROW_ASSIGN_OR_RAISE(state->regex_substring_matcher, + RegexSubstringMatcher::Make(converted_options, is_utf8)); + } + return MatchSubstringImpl::Exec( + ctx, batch, out, state->regex_substring_matcher.get()); #else return Status::NotImplemented("ignore_case requires RE2"); #endif } - ARROW_ASSIGN_OR_RAISE(auto matcher, PlainStartsWithMatcher::Make(options)); - return MatchSubstringImpl::Exec(ctx, batch, out, - matcher.get()); + if (!state->plain_starts_with_matcher) { + ARROW_ASSIGN_OR_RAISE(state->plain_starts_with_matcher, + PlainStartsWithMatcher::Make(state->options)); + } + return MatchSubstringImpl::Exec( + ctx, batch, out, state->plain_starts_with_matcher.get()); } }; template struct MatchSubstring { static Status Exec(KernelContext* ctx, const ExecSpan& batch, ExecResult* out) { - auto options = MatchSubstringState::Get(ctx); - if (options.ignore_case) { + auto* state = MatchSubstringState::GetState(ctx); + if (state->options.ignore_case) { #ifdef ARROW_WITH_RE2 - MatchSubstringOptions converted_options = options; - converted_options.pattern = RE2::QuoteMeta(options.pattern) + "$"; - const bool is_utf8 = is_string_or_string_view(batch[0].type()->id()); - ARROW_ASSIGN_OR_RAISE(auto matcher, - RegexSubstringMatcher::Make(converted_options, is_utf8)); - return MatchSubstringImpl::Exec(ctx, batch, out, - matcher.get()); + if (!state->regex_substring_matcher) { + MatchSubstringOptions converted_options = state->options; + converted_options.pattern = RE2::QuoteMeta(state->options.pattern) + "$"; + const bool is_utf8 = is_string_or_string_view(batch[0].type()->id()); + ARROW_ASSIGN_OR_RAISE(state->regex_substring_matcher, + RegexSubstringMatcher::Make(converted_options, is_utf8)); + } + return MatchSubstringImpl::Exec( + ctx, batch, out, state->regex_substring_matcher.get()); #else return Status::NotImplemented("ignore_case requires RE2"); #endif } - ARROW_ASSIGN_OR_RAISE(auto matcher, PlainEndsWithMatcher::Make(options)); - return MatchSubstringImpl::Exec(ctx, batch, out, - matcher.get()); + if (!state->plain_ends_with_matcher) { + ARROW_ASSIGN_OR_RAISE(state->plain_ends_with_matcher, + PlainEndsWithMatcher::Make(state->options)); + } + return MatchSubstringImpl::Exec( + ctx, batch, out, state->plain_ends_with_matcher.get()); } }; @@ -1536,51 +1591,68 @@ struct MatchLike { "should only codegen on physical types"); static Status Exec(KernelContext* ctx, const ExecSpan& batch, ExecResult* out) { - ARROW_ASSIGN_OR_RAISE( - const auto like_constants, - MatchLikeConstants::Instance( - /*is_utf8=*/is_string_or_string_view(batch[0].type()->id()))); + auto* original_state = MatchSubstringState::GetState(ctx); + if (!original_state->like_state) { + ARROW_ASSIGN_OR_RAISE( + const auto like_constants, + MatchLikeConstants::Instance( + /*is_utf8=*/is_string_or_string_view(batch[0].type()->id()))); + + const auto& original_options = original_state->options; + std::string pattern; + bool matched = false; + if (!original_options.ignore_case) { + if ((matched = RE2::FullMatch(original_options.pattern, + like_constants->like_pattern_is_substring_match, + &pattern))) { + original_state->like_kind = MatchSubstringState::LikeKind::kSubstring; + original_state->like_state = std::make_unique( + MatchSubstringOptions{pattern, false}); + } else if ((matched = RE2::FullMatch(original_options.pattern, + like_constants->like_pattern_is_starts_with, + &pattern))) { + original_state->like_kind = MatchSubstringState::LikeKind::kStartsWith; + original_state->like_state = std::make_unique( + MatchSubstringOptions{pattern, false}); + } else if ((matched = RE2::FullMatch(original_options.pattern, + like_constants->like_pattern_is_ends_with, + &pattern))) { + original_state->like_kind = MatchSubstringState::LikeKind::kEndsWith; + original_state->like_state = std::make_unique( + MatchSubstringOptions{pattern, false}); + } + } - auto original_options = MatchSubstringState::Get(ctx); - auto original_state = ctx->state(); + if (!matched) { + original_state->like_kind = MatchSubstringState::LikeKind::kRegex; + original_state->like_state = + std::make_unique(MatchSubstringOptions{ + MakeLikeRegex(original_options), original_options.ignore_case}); + } + } + ctx->SetState(original_state->like_state.get()); Status status; - std::string pattern; - bool matched = false; - if (!original_options.ignore_case) { - if ((matched = RE2::FullMatch(original_options.pattern, - like_constants->like_pattern_is_substring_match, - &pattern))) { - MatchSubstringOptions converted_options{pattern, original_options.ignore_case}; - MatchSubstringState converted_state(converted_options); - ctx->SetState(&converted_state); + switch (original_state->like_kind) { + case MatchSubstringState::LikeKind::kSubstring: status = MatchSubstring::Exec(ctx, batch, out); - } else if ((matched = RE2::FullMatch(original_options.pattern, - like_constants->like_pattern_is_starts_with, - &pattern))) { - MatchSubstringOptions converted_options{pattern, original_options.ignore_case}; - MatchSubstringState converted_state(converted_options); - ctx->SetState(&converted_state); + break; + case MatchSubstringState::LikeKind::kStartsWith: status = MatchSubstring::Exec(ctx, batch, out); - } else if ((matched = RE2::FullMatch(original_options.pattern, - like_constants->like_pattern_is_ends_with, - &pattern))) { - MatchSubstringOptions converted_options{pattern, original_options.ignore_case}; - MatchSubstringState converted_state(converted_options); - ctx->SetState(&converted_state); + break; + case MatchSubstringState::LikeKind::kEndsWith: status = MatchSubstring::Exec(ctx, batch, out); - } - } - - if (!matched) { - MatchSubstringOptions converted_options{MakeLikeRegex(original_options), - original_options.ignore_case}; - MatchSubstringState converted_state(converted_options); - ctx->SetState(&converted_state); - status = MatchSubstring::Exec(ctx, batch, out); + break; + case MatchSubstringState::LikeKind::kRegex: + status = + MatchSubstring::Exec(ctx, batch, out); + break; + default: + status = Status::Invalid("Invalid like pattern kind"); + break; } ctx->SetState(original_state); return status;