diff --git a/cpp/include/cudf/strings/detail/find.hpp b/cpp/include/cudf/strings/detail/find.hpp new file mode 100644 index 000000000000..81791d04fd47 --- /dev/null +++ b/cpp/include/cudf/strings/detail/find.hpp @@ -0,0 +1,43 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include + +namespace cudf::strings::detail { + +/** + * @copydoc cudf::strings::contains + */ +std::unique_ptr contains(strings_column_view const& input, + string_scalar const& target, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr); + +/** + * @copydoc cudf::strings::starts_with + */ +std::unique_ptr starts_with(strings_column_view const& input, + string_scalar const& target, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr); + +/** + * @copydoc cudf::strings::ends_with + */ +std::unique_ptr ends_with(strings_column_view const& input, + string_scalar const& target, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr); + +/** + * @copydoc cudf::strings::count + */ +std::unique_ptr count(strings_column_view const& input, + string_scalar const& target, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr); + +} // namespace cudf::strings::detail diff --git a/cpp/include/cudf/strings/regex/flags.hpp b/cpp/include/cudf/strings/regex/flags.hpp index b4ece3d0e009..ff4e63dcdd15 100644 --- a/cpp/include/cudf/strings/regex/flags.hpp +++ b/cpp/include/cudf/strings/regex/flags.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2021-2026, NVIDIA CORPORATION. + * SPDX-FileCopyrightText: Copyright (c) 2021-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ #pragma once @@ -99,6 +99,16 @@ enum class capture_groups : uint32_t { NON_CAPTURE ///< Convert all capture groups to non-capture groups }; +/** + * @brief Fast-path classification for literal-only patterns + */ +enum class literal_fast_path : int8_t { + NONE, ///< pattern is not eligible for any literal fast-path + LITERAL_ONLY, ///< pattern is a simple character sequence (literal) + STARTS_WITH, ///< pattern is a literal preceded by a begin-of-line anchor + ENDS_WITH ///< pattern is a literal followed by an end-of-line anchor +}; + /** @} */ // end of doxygen group } // namespace strings } // namespace CUDF_EXPORT cudf diff --git a/cpp/include/cudf/strings/regex/regex_program.hpp b/cpp/include/cudf/strings/regex/regex_program.hpp index 78b75e51afdd..4abb085afcdb 100644 --- a/cpp/include/cudf/strings/regex/regex_program.hpp +++ b/cpp/include/cudf/strings/regex/regex_program.hpp @@ -106,6 +106,13 @@ struct regex_program { */ [[nodiscard]] std::size_t compute_working_memory_size(int32_t num_strings) const; + /** + * @brief Returns literal string if specific fast-path is possible + * + * @return Which fast-path is available and the associate literal string + */ + [[nodiscard]] std::pair get_literal_fast_path() const; + private: std::string _pattern; regex_flags _flags; diff --git a/cpp/src/strings/contains.cu b/cpp/src/strings/contains.cu index db768379626c..fd249d6ff3d0 100644 --- a/cpp/src/strings/contains.cu +++ b/cpp/src/strings/contains.cu @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2019-2025, NVIDIA CORPORATION. + * SPDX-FileCopyrightText: Copyright (c) 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ @@ -13,7 +13,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -80,6 +82,27 @@ std::unique_ptr contains_re(strings_column_view const& input, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { + // check for potential fast-paths + auto [fp, literal] = prog.get_literal_fast_path(); + switch (fp) { + case literal_fast_path::LITERAL_ONLY: { + auto const target = + cudf::string_scalar(literal, true, stream, cudf::get_current_device_resource_ref()); + return contains(input, target, stream, mr); + } + case literal_fast_path::STARTS_WITH: { + auto const target = + cudf::string_scalar(literal, true, stream, cudf::get_current_device_resource_ref()); + return starts_with(input, target, stream, mr); + } + case literal_fast_path::ENDS_WITH: { + auto const target = + cudf::string_scalar(literal, true, stream, cudf::get_current_device_resource_ref()); + return ends_with(input, target, stream, mr); + } + default: break; + } + return contains_impl(input, prog, false, stream, mr); } @@ -88,6 +111,13 @@ std::unique_ptr matches_re(strings_column_view const& input, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { + auto [fp, literal] = prog.get_literal_fast_path(); + if (fp == literal_fast_path::LITERAL_ONLY or fp == literal_fast_path::STARTS_WITH) { + auto const target = + cudf::string_scalar(literal, true, stream, cudf::get_current_device_resource_ref()); + return starts_with(input, target, stream, mr); + } + return contains_impl(input, prog, true, stream, mr); } @@ -96,7 +126,13 @@ std::unique_ptr count_re(strings_column_view const& input, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { - // create device object from regex_program + auto [fp, literal] = prog.get_literal_fast_path(); + if (fp == literal_fast_path::LITERAL_ONLY) { + auto const target = + cudf::string_scalar(literal, true, stream, cudf::get_current_device_resource_ref()); + return count(input, target, stream, mr); + } + auto d_prog = regex_device_builder::create_prog_device(prog, stream); auto const d_strings = column_device_view::create(input.parent(), stream); diff --git a/cpp/src/strings/regex/regcomp.cpp b/cpp/src/strings/regex/regcomp.cpp index a065799b0ae9..ecc7635ac3c5 100644 --- a/cpp/src/strings/regex/regcomp.cpp +++ b/cpp/src/strings/regex/regcomp.cpp @@ -1220,6 +1220,50 @@ void reprog::check_for_errors() } } +std::pair reprog::check_for_literal_fast_path() const +{ + if (_flags != regex_flags::DEFAULT) { return {literal_fast_path::NONE, {}}; } + if (_startinst_ids.size() > 2) { return {literal_fast_path::NONE, {}}; } + auto const count = static_cast(_insts.size()); + if (count < 2) { return {literal_fast_path::NONE, {}}; } + + auto inst = _insts[_startinst_id]; + + // Optional BOL at the start of the pattern + bool const has_bol = (inst.type == BOL); + if (has_bol) { + auto const id = inst.u2.next_id; + if (id < 0 || id >= count) { return {literal_fast_path::NONE, {}}; } + inst = _insts[id]; + } + + // Accumulate sequential CHAR bytes + std::string literal; + while (inst.type == CHAR && inst.u1.c != 0) { + std::array utf8 = {}; + utf8[from_char_utf8(inst.u1.c, utf8.data())] = 0; + literal += utf8.data(); + auto const id = inst.u2.next_id; + if (id < 0 || id >= count) { return {literal_fast_path::NONE, {}}; } + inst = _insts[id]; + } + if (literal.empty()) { return {literal_fast_path::NONE, {}}; } + + // If we are at END then we are literal-only or starts-with. + if (inst.type == END) { + return {has_bol ? literal_fast_path::STARTS_WITH : literal_fast_path::LITERAL_ONLY, + std::move(literal)}; + } + // Final check for ends-with: EOL followed by END + if (!has_bol && inst.type == EOL && inst.u1.c == 'Z') { + auto const id = inst.u2.next_id; + if (id >= 0 && id < count && _insts[id].type == END) { + return {literal_fast_path::ENDS_WITH, std::move(literal)}; + } + } + return {literal_fast_path::NONE, {}}; +} + match_flags reprog::compute_match_flags() const { static std::unordered_set const non_consuming_inst_types{ @@ -1346,6 +1390,14 @@ void reprog::print() const printf("\n"); } if (_num_capturing_groups) { printf("Number of capturing groups: %d\n", _num_capturing_groups); } + + auto [fp, literal] = check_for_literal_fast_path(); + switch (fp) { + case literal_fast_path::LITERAL_ONLY: printf("literal-only: %s\n", literal.c_str()); break; + case literal_fast_path::STARTS_WITH: printf("starts-with: %s\n", literal.c_str()); break; + case literal_fast_path::ENDS_WITH: printf("ends-with: %s\n", literal.c_str()); break; + default: break; + } } #endif diff --git a/cpp/src/strings/regex/regcomp.h b/cpp/src/strings/regex/regcomp.h index 0ce4df885d39..af3a981ae952 100644 --- a/cpp/src/strings/regex/regcomp.h +++ b/cpp/src/strings/regex/regcomp.h @@ -135,6 +135,7 @@ class reprog { void finalize(); void check_for_errors(); + [[nodiscard]] std::pair check_for_literal_fast_path() const; [[nodiscard]] match_flags compute_match_flags() const; #ifndef NDEBUG @@ -147,7 +148,7 @@ class reprog { int32_t _startinst_id{}; // id of first instruction std::vector _startinst_ids; // short-cut to speed-up ORs int32_t _num_capturing_groups{}; - [[maybe_unused]] regex_flags _flags{}; + regex_flags _flags{}; reprog(regex_flags); void collapse_nops(); diff --git a/cpp/src/strings/regex/regex_program.cpp b/cpp/src/strings/regex/regex_program.cpp index 2cf6abeed689..1c0dd99acfc6 100644 --- a/cpp/src/strings/regex/regex_program.cpp +++ b/cpp/src/strings/regex/regex_program.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2025, NVIDIA CORPORATION. + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ @@ -48,5 +48,10 @@ std::size_t regex_program::compute_working_memory_size(int32_t num_strings) cons return detail::compute_working_memory_size(num_strings, instructions_count()); } +std::pair regex_program::get_literal_fast_path() const +{ + return _impl->prog.check_for_literal_fast_path(); +} + } // namespace strings } // namespace cudf diff --git a/cpp/src/strings/replace/replace_re.cu b/cpp/src/strings/replace/replace_re.cu index fee76d85eb9d..d1500bc4a65d 100644 --- a/cpp/src/strings/replace/replace_re.cu +++ b/cpp/src/strings/replace/replace_re.cu @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2019-2025, NVIDIA CORPORATION. + * SPDX-FileCopyrightText: Copyright (c) 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -95,16 +96,21 @@ std::unique_ptr replace_re(strings_column_view const& input, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr) { - if (input.is_empty()) return make_empty_column(type_id::STRING); + if (input.is_empty()) { return make_empty_column(type_id::STRING); } CUDF_EXPECTS(replacement.is_valid(stream), "Parameter replacement must be valid"); - string_view d_repl(replacement.data(), replacement.size()); - - // create device object from regex_program - auto d_prog = regex_device_builder::create_prog_device(prog, stream); auto const maxrepl = max_replace_count.value_or(-1); + auto [fp, literal] = prog.get_literal_fast_path(); + if (fp == literal_fast_path::LITERAL_ONLY) { + auto const target = + cudf::string_scalar(literal, true, stream, cudf::get_current_device_resource_ref()); + return replace(input, target, replacement, maxrepl, stream, mr); + } + + auto const d_repl = string_view(replacement.data(), replacement.size()); + auto const d_prog = regex_device_builder::create_prog_device(prog, stream); auto const d_strings = column_device_view::create(input.parent(), stream); auto [offsets_column, chars] = make_strings_children( diff --git a/cpp/src/strings/search/count.cu b/cpp/src/strings/search/count.cu index d0d994e6ce8c..e42f9e383fe0 100644 --- a/cpp/src/strings/search/count.cu +++ b/cpp/src/strings/search/count.cu @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -79,7 +80,6 @@ std::unique_ptr count(strings_column_view const& input, return results; } - } // namespace detail // external APIs diff --git a/cpp/tests/strings/contains_tests.cpp b/cpp/tests/strings/contains_tests.cpp index 89ed9756d00a..6b338ec502d0 100644 --- a/cpp/tests/strings/contains_tests.cpp +++ b/cpp/tests/strings/contains_tests.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2019-2026, NVIDIA CORPORATION. + * SPDX-FileCopyrightText: Copyright (c) 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ @@ -373,7 +373,7 @@ TEST_F(StringsContainsTests, Errors) TEST_F(StringsContainsTests, CountTest) { std::vector h_strings{ - "The quick brown @fox jumps ovér the", "lazy @dog", "1:2:3:4", "00:0:00", nullptr, ""}; + "The quick brown @fox jumps ovér the", "lazy @dog lazy", "1:2:3:4", "00:0:00", nullptr, ""}; cudf::test::strings_column_wrapper strings( h_strings.begin(), h_strings.end(), cudf::test::iterators::nulls_from_nullptrs(h_strings)); @@ -402,6 +402,22 @@ TEST_F(StringsContainsTests, CountTest) auto results = cudf::strings::count_re(strings_view, *prog); CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); } + { + auto pattern = std::string("o"); + cudf::test::fixed_width_column_wrapper expected( + {3, 1, 0, 0, 0, 0}, cudf::test::iterators::nulls_from_nullptrs(h_strings)); + auto prog = cudf::strings::regex_program::create(pattern); + auto results = cudf::strings::count_re(strings_view, *prog); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); + } + { + auto pattern = std::string("\\blazy\\b"); + cudf::test::fixed_width_column_wrapper expected( + {0, 2, 0, 0, 0, 0}, cudf::test::iterators::nulls_from_nullptrs(h_strings)); + auto prog = cudf::strings::regex_program::create(pattern); + auto results = cudf::strings::count_re(strings_view, *prog); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); + } } TEST_F(StringsContainsTests, CountEmptyMatching) @@ -787,6 +803,16 @@ TEST_F(StringsContainsTests, EndOfString) CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(*results, expected_count); results = cudf::strings::count_re(view, *prog_ml); CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(*results, expected_count); + + pattern = std::string("abé$"); + prog = cudf::strings::regex_program::create(pattern); + expected = cudf::test::fixed_width_column_wrapper({1, 0, 1, 0, 1, 1}); + results = cudf::strings::contains_re(view, *prog); + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(*results, expected); + prog_ml = cudf::strings::regex_program::create(pattern, cudf::strings::regex_flags::MULTILINE); + expected = cudf::test::fixed_width_column_wrapper({1, 1, 1, 0, 1, 1}); + results = cudf::strings::contains_re(view, *prog_ml); + CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(*results, expected); } TEST_F(StringsContainsTests, DotAll)