This is an automated email from the ASF dual-hosted git repository.
Mryange pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 707ed45ee8c [improvement](be) Optimize string functions and ARM byte
checks (#69223)
707ed45ee8c is described below
commit 707ed45ee8cfde440a1c14ea85b78bc8be9339ce
Author: HappenLee <[email protected]>
AuthorDate: Thu Oct 22 12:17:00 2026 +0800
[improvement](be) Optimize string functions and ARM byte checks (#69223)
### What problem does this PR solve?
Issue Number: N/A
Related PR: N/A
Problem Summary:
String functions incur avoidable work on mostly ASCII columns and
constant arguments. This change dispatches ASCII rows in mixed columns
to the existing case-conversion fast paths, adds a table-based translate
path for constant ASCII mappings, appends concat_ws inputs directly,
reuses StringOP::fast_repeat with reserved output storage, and caches
constant parse_url part lookup. It also adds AArch64 NEON
implementations for contain_one/contain_zero with guard-page and
boundary tests.
Non-ASCII case conversion retains full ICU root-locale mappings,
including expansions and contextual casing. parse_url still propagates
NULL before reporting an invalid URL part. Unit tests cover mixed
translate replacement/deletion and NULL URL/key inputs with invalid
parts. Benchmark speedups have not been measured in this environment.
### Release note
Reduce processing overhead in string case conversion, translate,
concat_ws, repeat, parse_url, and AArch64 byte-presence checks while
preserving SQL results.
### Check List (For Author)
- Test:
- Unit tests added; execution is pending. Attempted the relevant BE ASAN
tests with `run-be-ut.sh -j 48 --run --filter=...`, but the build
stopped before running tests because the local third-party package lacks
`libpaimon_c.a`.
- Passed `build-support/check-format.sh` with clang-format 16.0.6,
`build-support/check-build-hygiene.sh`, and `git diff --check`.
- Attempted `build-support/run-clang-tidy.sh`; analysis is incomplete
because the failed build did not produce the required PCH, and the local
clang-tidy cannot resolve the compiler resource header `stddef.h`.
- AArch64 execution and performance benchmarks were not run.
- Behavior changed: No SQL semantic changes intended.
- Does this need documentation: No.
### Check List (For Reviewer who merge this PR)
- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label
---
be/src/exprs/function/function_string.cpp | 122 ++++++++++++++++------
be/src/exprs/function/function_string_concat.h | 74 +++++++++-----
be/src/exprs/function/function_string_misc.cpp | 93 +++++++++++++++--
be/src/exprs/function/function_string_url.cpp | 17 +++-
be/src/util/simd/bits.h | 12 +++
be/test/exprs/function/function_string_test.cpp | 130 ++++++++++++++++++++++--
be/test/util/simd/bits_test.cpp | 67 ++++++++++++
7 files changed, 442 insertions(+), 73 deletions(-)
diff --git a/be/src/exprs/function/function_string.cpp
b/be/src/exprs/function/function_string.cpp
index d62573112bb..5068f3df698 100644
--- a/be/src/exprs/function/function_string.cpp
+++ b/be/src/exprs/function/function_string.cpp
@@ -572,42 +572,78 @@ struct TransferImpl {
return Status::OK();
}
- const bool is_ascii = simd::VStringFunctions::is_ascii({data.data(),
data.size()});
res_offsets.resize(offset_size);
- if (is_ascii) {
+ const size_t data_length = data.size();
+ // Fast path: entire column is ASCII
+ if (simd::VStringFunctions::is_ascii({data.data(), data.size()})) {
memcpy_small_allow_read_write_overflow15(
res_offsets.data(), offsets.data(),
offset_size * sizeof(ColumnString::Offsets::value_type));
- size_t data_length = data.size();
res_data.resize(data_length);
if constexpr (std::is_same_v<OpName, NameToUpper>) {
simd::VStringFunctions::to_upper(data.data(), data_length,
res_data.data());
} else if constexpr (std::is_same_v<OpName, NameToLower>) {
simd::VStringFunctions::to_lower(data.data(), data_length,
res_data.data());
}
- } else {
- execute_utf8(data, offsets, res_data, res_offsets);
+ return Status::OK();
}
+ // Mixed path: combine consecutive ASCII rows for SIMD; only non-ASCII
rows use ICU.
+ // Preserve full Unicode case mappings for non-ASCII rows.
+ execute_mixed(data, offsets, res_data, res_offsets);
return Status::OK();
}
- static void execute_utf8(const ColumnString::Chars& data, const
ColumnString::Offsets& offsets,
- ColumnString::Chars& res_data,
ColumnString::Offsets& res_offsets) {
+ static void execute_mixed(const ColumnString::Chars& data, const
ColumnString::Offsets& offsets,
+ ColumnString::Chars& res_data,
ColumnString::Offsets& res_offsets) {
+ size_t offset_size = offsets.size();
+ res_data.reserve(data.size());
+ auto process_ascii_run = [&](int64_t first_row, int64_t last_row) {
+ if (first_row == last_row) {
+ return;
+ }
+ const size_t src_offset = offsets[first_row - 1];
+ const size_t size = offsets[last_row - 1] - src_offset;
+ const size_t res_offset = res_data.size();
+ ColumnString::check_chars_length(res_offset + size,
res_offsets.size());
+ res_data.resize(res_offset + size);
+ if constexpr (std::is_same_v<OpName, NameToUpper>) {
+ simd::VStringFunctions::to_upper(data.data() + src_offset,
size,
+ res_data.data() + res_offset);
+ } else if constexpr (std::is_same_v<OpName, NameToLower>) {
+ simd::VStringFunctions::to_lower(data.data() + src_offset,
size,
+ res_data.data() + res_offset);
+ }
+ // Earlier Unicode rows can change length, so rebase each offset
on this run's output.
+ for (int64_t row = first_row; row < last_row; ++row) {
+ res_offsets[row] = (ColumnString::Offset)(res_offset +
offsets[row] - src_offset);
+ }
+ };
+
+ int64_t ascii_run_start = 0;
std::string result;
- for (int64_t i = 0; i < offsets.size(); ++i) {
- const char* begin = reinterpret_cast<const char*>(&data[offsets[i
- 1]]);
+ for (int64_t i = 0; i < offset_size; ++i) {
+ const uint8_t* begin = &data[offsets[i - 1]];
uint32_t size = offsets[i] - offsets[i - 1];
+ if (size == 0 ||
+ simd::VStringFunctions::is_ascii({reinterpret_cast<const
char*>(begin), size})) {
+ continue;
+ }
+ // E.g. ["a", "b", "你", "c"] converts "ab" together, keeping short
rows SIMD-friendly.
+ process_ascii_run(ascii_run_start, i);
+ ascii_run_start = i + 1;
+ // Preserve full Unicode mappings, including expansions and
context-sensitive casing.
result.clear();
if constexpr (std::is_same_v<OpName, NameToUpper>) {
- to_upper_utf8(begin, size, result);
+ to_upper_utf8(reinterpret_cast<const char*>(begin), size,
result);
} else if constexpr (std::is_same_v<OpName, NameToLower>) {
- to_lower_utf8(begin, size, result);
+ to_lower_utf8(reinterpret_cast<const char*>(begin), size,
result);
}
StringOP::push_value_string(result, i, res_data, res_offsets);
}
+ process_ascii_run(ascii_run_start, offset_size);
}
static void to_upper_utf8(const char* data, uint32_t size, std::string&
result) {
@@ -637,15 +673,55 @@ struct InitcapImpl {
ColumnString::Chars& res_data, ColumnString::Offsets&
res_offsets) {
res_offsets.resize(offsets.size());
- const bool is_ascii = simd::VStringFunctions::is_ascii({data.data(),
data.size()});
- if (is_ascii) {
+ // Fast path: entire column is ASCII
+ if (simd::VStringFunctions::is_ascii({data.data(), data.size()})) {
impl_vectors_ascii(data, offsets, res_data, res_offsets);
- } else {
- impl_vectors_utf8(data, offsets, res_data, res_offsets);
+ return Status::OK();
}
+ // Mixed path: per-row decision so ASCII rows never touch ICU
+ impl_vectors_mixed(data, offsets, res_data, res_offsets);
return Status::OK();
}
+ static void impl_vectors_mixed(const ColumnString::Chars& data,
+ const ColumnString::Offsets& offsets,
+ ColumnString::Chars& res_data,
+ ColumnString::Offsets& res_offsets) {
+ size_t offset_size = offsets.size();
+ res_data.reserve(data.size());
+ std::string result;
+ for (int64_t i = 0; i < offset_size; ++i) {
+ const uint8_t* begin = &data[offsets[i - 1]];
+ uint32_t size = offsets[i] - offsets[i - 1];
+ if (size == 0) {
+ res_offsets[i] = (ColumnString::Offset)res_data.size();
+ continue;
+ }
+ if (simd::VStringFunctions::is_ascii({reinterpret_cast<const
char*>(begin), size})) {
+ size_t res_offset = res_data.size();
+ ColumnString::check_chars_length(res_offset + size,
res_offsets.size());
+ res_data.resize(res_offset + size);
+ // to_lower then capitalize first letter of each word
+ simd::VStringFunctions::to_lower(begin, size, res_data.data()
+ res_offset);
+ bool need_capitalize = true;
+ for (size_t j = 0; j < size; ++j) {
+ auto& c = res_data[res_offset + j];
+ if (!::isalnum(c)) {
+ need_capitalize = true;
+ } else if (need_capitalize) {
+ c = static_cast<char>(::toupper(static_cast<unsigned
char>(c)));
+ need_capitalize = false;
+ }
+ }
+ res_offsets[i] = (ColumnString::Offset)res_data.size();
+ } else {
+ result.clear();
+ to_initcap_utf8(reinterpret_cast<const char*>(begin), size,
result);
+ StringOP::push_value_string(result, i, res_data, res_offsets);
+ }
+ }
+ }
+
static void impl_vectors_ascii(const ColumnString::Chars& data,
const ColumnString::Offsets& offsets,
ColumnString::Chars& res_data,
@@ -670,7 +746,7 @@ struct InitcapImpl {
} else if (need_capitalize) {
/*
https://en.cppreference.com/w/cpp/string/byte/toupper
- Like all other functions from <cctype>, the behavior of
std::toupper is undefined if the argument's value is neither representable as
unsigned char nor equal to EOF.
+ Like all other functions from <cctype>, the behavior of
std::toupper is undefined if the argument's value is neither representable as
unsigned char nor equal to EOF.
To use these functions safely with plain chars (or signed
chars), the argument should first be converted to unsigned char:
char my_toupper(char ch)
{
@@ -686,20 +762,6 @@ struct InitcapImpl {
}
}
- static void impl_vectors_utf8(const ColumnString::Chars& data,
- const ColumnString::Offsets& offsets,
- ColumnString::Chars& res_data,
- ColumnString::Offsets& res_offsets) {
- std::string result;
- for (int64_t i = 0; i < offsets.size(); ++i) {
- const char* begin = reinterpret_cast<const char*>(&data[offsets[i
- 1]]);
- uint32_t size = offsets[i] - offsets[i - 1];
- result.clear();
- to_initcap_utf8(begin, size, result);
- StringOP::push_value_string(result, i, res_data, res_offsets);
- }
- }
-
static void to_initcap_utf8(const char* data, uint32_t size, std::string&
result) {
icu::StringPiece sp;
sp.set(data, size);
diff --git a/be/src/exprs/function/function_string_concat.h
b/be/src/exprs/function/function_string_concat.h
index 6dd043a6033..d109d78a048 100644
--- a/be/src/exprs/function/function_string_concat.h
+++ b/be/src/exprs/function/function_string_concat.h
@@ -520,7 +520,16 @@ private:
}
}
- fmt::format_to(buffer, "{}", fmt::join(views, sep));
+ // Direct append instead of fmt::format_to + fmt::join
+ if (!views.empty()) {
+ buffer.append(views[0].data(), views[0].data() +
views[0].size());
+ for (size_t j = 1; j < views.size(); ++j) {
+ if (!sep.empty()) {
+ buffer.append(sep.data(), sep.data() + sep.size());
+ }
+ buffer.append(views[j].data(), views[j].data() +
views[j].size());
+ }
+ }
StringOP::push_value_string(std::string_view(buffer.data(),
buffer.size()), i, res_data,
res_offset);
@@ -560,7 +569,16 @@ private:
views.emplace_back(ptr, size);
}
}
- fmt::format_to(buffer, "{}", fmt::join(views, sep));
+ // Direct append instead of fmt::format_to + fmt::join
+ if (!views.empty()) {
+ buffer.append(views[0].data(), views[0].data() +
views[0].size());
+ for (size_t j = 1; j < views.size(); ++j) {
+ if (!sep.empty()) {
+ buffer.append(sep.data(), sep.data() + sep.size());
+ }
+ buffer.append(views[j].data(), views[j].data() +
views[j].size());
+ }
+ }
StringOP::push_value_string(std::string_view(buffer.data(),
buffer.size()), i, res_data,
res_offset);
}
@@ -626,50 +644,60 @@ public:
ColumnUInt8::Container& null_map) const {
size_t input_row_size = offsets.size();
- fmt::memory_buffer buffer;
res_offsets.resize(input_row_size);
null_map.resize_fill(input_row_size, 0);
+ // Reserve the combined size of rows with positive repeat counts.
+ size_t total = 0;
for (ssize_t i = 0; i < input_row_size; ++i) {
- buffer.clear();
- const char* raw_str = reinterpret_cast<const
char*>(&data[offsets[i - 1]]);
+ size_t size = offsets[i] - offsets[i - 1];
+ int repeat = repeats[i];
+ if (repeat > 0) {
+ ColumnString::check_chars_length(repeat * size + total, 0);
+ total += repeat * size;
+ }
+ }
+ res_data.reserve(total);
+
+ for (ssize_t i = 0; i < input_row_size; ++i) {
+ const uint8_t* raw_str = &data[offsets[i - 1]];
size_t size = offsets[i] - offsets[i - 1];
int repeat = repeats[i];
if (repeat <= 0) {
StringOP::push_empty_string(i, res_data, res_offsets);
} else {
- ColumnString::check_chars_length(repeat * size +
res_data.size(), 0);
- for (int j = 0; j < repeat; ++j) {
- buffer.append(raw_str, raw_str + size);
- }
- StringOP::push_value_string(std::string_view(buffer.data(),
buffer.size()), i,
- res_data, res_offsets);
+ size_t old_size = res_data.size();
+ res_data.resize(old_size + repeat * size);
+ StringOP::fast_repeat(res_data.data() + old_size, raw_str,
size, repeat);
+ res_offsets[i] = (ColumnString::Offset)res_data.size();
}
}
return Status::OK();
}
- // TODO: 1. use pmr::vector<char> replace fmt_buffer may speed up the code
- // 2. abstract the `vector_vector` and `vector_const`
- // 3. rethink we should use `DEFAULT_MAX_STRING_SIZE` to bigger here
+ // TODO: rethink we should use `DEFAULT_MAX_STRING_SIZE` to bigger here
void vector_const(const ColumnString::Chars& data, const
ColumnString::Offsets& offsets,
int repeat, ColumnString::Chars& res_data,
ColumnString::Offsets& res_offsets,
ColumnUInt8::Container& null_map) const {
size_t input_row_size = offsets.size();
- fmt::memory_buffer buffer;
res_offsets.resize(input_row_size);
null_map.resize_fill(input_row_size, 0);
+ // Pre-size for const repeat
+ size_t total = 0;
for (ssize_t i = 0; i < input_row_size; ++i) {
- buffer.clear();
- const char* raw_str = reinterpret_cast<const
char*>(&data[offsets[i - 1]]);
size_t size = offsets[i] - offsets[i - 1];
- ColumnString::check_chars_length(repeat * size + res_data.size(),
0);
+ ColumnString::check_chars_length(repeat * size + total, 0);
+ total += repeat * size;
+ }
+ res_data.reserve(total);
- for (int j = 0; j < repeat; ++j) {
- buffer.append(raw_str, raw_str + size);
- }
- StringOP::push_value_string(std::string_view(buffer.data(),
buffer.size()), i, res_data,
- res_offsets);
+ for (ssize_t i = 0; i < input_row_size; ++i) {
+ const uint8_t* raw_str = &data[offsets[i - 1]];
+ size_t size = offsets[i] - offsets[i - 1];
+ size_t old_size = res_data.size();
+ res_data.resize(old_size + repeat * size);
+ StringOP::fast_repeat(res_data.data() + old_size, raw_str, size,
repeat);
+ res_offsets[i] = (ColumnString::Offset)res_data.size();
}
}
};
diff --git a/be/src/exprs/function/function_string_misc.cpp
b/be/src/exprs/function/function_string_misc.cpp
index e6bdbb50f38..131cc74acbc 100644
--- a/be/src/exprs/function/function_string_misc.cpp
+++ b/be/src/exprs/function/function_string_misc.cpp
@@ -969,14 +969,23 @@ public:
const auto* col_from = assert_cast<const
ColumnString*>(argument_columns[1].get());
const auto* col_to = assert_cast<const
ColumnString*>(argument_columns[2].get());
- bool is_ascii = col_source->is_ascii() && col_from->is_ascii() &&
col_to->is_ascii();
auto impl_vectors = impl_vectors_utf8<false>;
- if (col_const[1] && col_const[2] && is_ascii) {
- impl_vectors = impl_vectors_ascii<true>;
- } else if (col_const[1] && col_const[2]) {
- impl_vectors = impl_vectors_utf8<true>;
- } else if (is_ascii) {
- impl_vectors = impl_vectors_ascii<false>;
+ if (col_const[1] && col_const[2]) {
+ // Non-ASCII mappings require UTF-8 processing regardless of the
source.
+ const bool mapping_ascii = col_from->is_ascii() &&
col_to->is_ascii();
+ if (mapping_ascii) {
+ const bool source_ascii = col_source->is_ascii();
+ impl_vectors = source_ascii ? impl_vectors_ascii<true> :
impl_vectors_mixed<true>;
+ } else {
+ impl_vectors = impl_vectors_utf8<true>;
+ }
+ } else {
+ // A non-ASCII source needs no mapping scans when the mappings
vary by row.
+ const bool all_ascii =
+ col_source->is_ascii() && col_from->is_ascii() &&
col_to->is_ascii();
+ if (all_ascii) {
+ impl_vectors = impl_vectors_ascii<false>;
+ }
}
impl_vectors(col_source, col_from, col_to, col_res.get());
block.get_by_position(result).column = std::move(col_res);
@@ -984,6 +993,69 @@ public:
}
private:
+ static bool bytes_is_ascii(const char* data, size_t size) {
+ return simd::VStringFunctions::is_ascii({data, size});
+ }
+
+ // Per-row dispatch: ASCII source rows use the 128-byte table; only
non-ASCII
+ // rows fall back to the UTF-8 map. Requires from/to to be ASCII.
+ template <bool IsConst>
+ static void impl_vectors_mixed(const ColumnString* col_source, const
ColumnString* col_from,
+ const ColumnString* col_to, ColumnString*
col_res) {
+ auto& res_chars = col_res->get_chars();
+ auto& res_offsets = col_res->get_offsets();
+ res_chars.reserve(col_source->get_chars().size());
+ res_offsets.reserve(col_source->get_offsets().size());
+ DCHECK_EQ(col_res->size(), 0);
+ AsciiMap map;
+ std::unordered_map<std::string_view, std::string_view> translate_map;
+ if (IsConst) {
+ const auto& from_str = col_from->get_data_at(0);
+ const auto& to_str = col_to->get_data_at(0);
+ if (!build_translate_map_ascii(map, from_str, to_str)) {
+ // No deletes: whole-column table translate; high bytes
(>=128) are
+ // identity in the map so multi-byte UTF-8 sequences are
preserved.
+ res_offsets.insert(col_source->get_offsets().begin(),
+ col_source->get_offsets().end());
+ res_chars.insert(col_source->get_chars().begin(),
col_source->get_chars().end());
+ for (size_t i = 0; i < res_chars.size(); ++i) {
+ auto c = static_cast<UInt8>(res_chars[i]);
+ if (c < 128) {
+ res_chars[i] = static_cast<char>(map[c]);
+ }
+ }
+ return;
+ }
+ translate_map =
+ build_translate_map_utf8(from_str.to_string_view(),
to_str.to_string_view());
+ }
+
+ std::string row_buf;
+ for (size_t i = 0; i < col_source->size(); ++i) {
+ const auto& source_str = col_source->get_data_at(i);
+ if (!IsConst) {
+ const auto& from_str = col_from->get_data_at(i);
+ const auto& to_str = col_to->get_data_at(i);
+ build_translate_map_ascii(map, from_str, to_str);
+ translate_map =
build_translate_map_utf8(from_str.to_string_view(),
+
to_str.to_string_view());
+ }
+ if (bytes_is_ascii(source_str.data, source_str.size)) {
+ // ASCII row: O(n) table lookup, no hashing
+ row_buf.clear();
+ row_buf.resize(source_str.size);
+ auto written =
+ translate_ascii(source_str, map,
reinterpret_cast<UInt8*>(row_buf.data()));
+ row_buf.resize(written);
+ col_res->insert_data(row_buf.data(), row_buf.size());
+ } else {
+ // Non-ASCII row: UTF-8 map (built once when IsConst)
+ auto translated_str =
translate_utf8(source_str.to_string_view(), translate_map);
+ col_res->insert_data(translated_str.data(),
translated_str.size());
+ }
+ }
+ }
+
template <bool IsConst>
static void impl_vectors_ascii(const ColumnString* col_source, const
ColumnString* col_from,
const ColumnString* col_to, ColumnString*
col_res) {
@@ -1118,9 +1190,10 @@ private:
for (size_t i = 0, char_size = 0; i < source_str.size(); i +=
char_size) {
char_size = get_utf8_byte_length(source_str[i]);
auto c = source_str.substr(i, char_size);
- if (translate_map.find(c) != translate_map.end()) {
- if (!translate_map[c].empty()) {
- result.append(translate_map[c]);
+ auto it = translate_map.find(c);
+ if (it != translate_map.end()) {
+ if (!it->second.empty()) {
+ result.append(it->second);
}
} else {
result.append(c);
diff --git a/be/src/exprs/function/function_string_url.cpp
b/be/src/exprs/function/function_string_url.cpp
index dcdaa6fccf7..7d1613fb762 100644
--- a/be/src/exprs/function/function_string_url.cpp
+++ b/be/src/exprs/function/function_string_url.cpp
@@ -115,6 +115,13 @@ public:
ColumnView<TYPE_STRING>::create(block.get_by_position(arguments[0]).column);
const auto part_col =
ColumnView<TYPE_STRING>::create(block.get_by_position(arguments[1]).column);
+ const bool part_is_const = part_col.is_const;
+ UrlParser::UrlPart const_url_part = UrlParser::INVALID;
+ if (part_is_const && input_rows_count > 0 && !part_col.is_null_at(0)) {
+ const_url_part = UrlParser::get_url_part(part_col.value_at(0));
+ }
+ // Validate after the row's null checks: NULL inputs must still
produce NULL
+ // even when a constant URL part is invalid.
if (has_key) {
const auto key_col =
ColumnView<TYPE_STRING>::create(block.get_by_position(arguments[2]).column);
@@ -123,9 +130,10 @@ public:
StringOP::push_null_string(i, res_chars, res_offsets,
null_map_data);
continue;
}
- const auto part = part_col.value_at(i);
- const auto url_part = UrlParser::get_url_part(part);
+ const auto url_part = part_is_const ? const_url_part
+ :
UrlParser::get_url_part(part_col.value_at(i));
if (url_part == UrlParser::INVALID) {
+ const auto part = part_col.value_at(i);
return Status::RuntimeError("Invalid URL part: {}\n{}",
std::string(part.data,
part.size),
"(Valid URL parts are
'PROTOCOL', 'HOST', "
@@ -147,9 +155,10 @@ public:
StringOP::push_null_string(i, res_chars, res_offsets,
null_map_data);
continue;
}
- const auto part = part_col.value_at(i);
- const auto url_part = UrlParser::get_url_part(part);
+ const auto url_part = part_is_const ? const_url_part
+ :
UrlParser::get_url_part(part_col.value_at(i));
if (url_part == UrlParser::INVALID) {
+ const auto part = part_col.value_at(i);
return Status::RuntimeError("Invalid URL part: {}\n{}",
std::string(part.data,
part.size),
"(Valid URL parts are
'PROTOCOL', 'HOST', "
diff --git a/be/src/util/simd/bits.h b/be/src/util/simd/bits.h
index a87d367a865..41b999b0be1 100644
--- a/be/src/util/simd/bits.h
+++ b/be/src/util/simd/bits.h
@@ -289,6 +289,12 @@ inline bool contain_one(const uint8_t* __restrict data,
size_t size) {
return true;
}
}
+#elif defined(__ARM_NEON) && defined(__aarch64__)
+ for (; i + 16 <= size; i += 16) {
+ if (vmaxvq_u8(vld1q_u8(data + i)) != 0) {
+ return true;
+ }
+ }
#endif
for (; i < size; ++i) {
if (data[i]) {
@@ -316,6 +322,12 @@ inline bool contain_zero(const uint8_t* __restrict data,
size_t size) {
return true;
}
}
+#elif defined(__ARM_NEON) && defined(__aarch64__)
+ for (; i + 16 <= size; i += 16) {
+ if (vminvq_u8(vld1q_u8(data + i)) == 0) {
+ return true;
+ }
+ }
#endif
for (; i < size; ++i) {
if (!data[i]) {
diff --git a/be/test/exprs/function/function_string_test.cpp
b/be/test/exprs/function/function_string_test.cpp
index 097cd78e668..66d1a3ca3d3 100644
--- a/be/test/exprs/function/function_string_test.cpp
+++ b/be/test/exprs/function/function_string_test.cpp
@@ -24,11 +24,14 @@
#include <string>
#include <vector>
+#include "core/block/block.h"
+#include "core/data_type/data_type_nullable.h"
#include "core/data_type/data_type_number.h"
#include "core/data_type/data_type_string.h"
#include "core/field.h"
#include "core/types.h"
#include "exprs/function/function_test_util.h"
+#include "exprs/function/simple_function_factory.h"
#include "util/defer_op.h"
#include "util/encryption_util.h"
#include "util/md5.h"
@@ -744,17 +747,62 @@ TEST(function_string_test, function_string_upper_test) {
}
}
+TEST(function_string_test, function_string_case_mixed_ascii_runs_test) {
+ InputTypeSet input_types = {PrimitiveType::TYPE_VARCHAR};
+ DataSet lower_data = {{{std::string("")}, std::string("")}};
+ DataSet upper_data = lower_data;
+ // Each row is shorter than a SIMD register, but the consecutive run spans
multiple registers.
+ for (int i = 0; i < 32; ++i) {
+ lower_data.push_back({{std::string("aB")}, std::string("ab")});
+ upper_data.push_back({{std::string("aB")}, std::string("AB")});
+ }
+ lower_data.insert(
+ lower_data.end(),
+ {
+ {{std::string("\u0130")},
+ std::string("i\u0307")}, // UTF-8 grows from 2 to 3 bytes.
+ {{std::string("")}, std::string("")},
+ {{std::string("AFTER")}, std::string("after")},
+ {{Null()}, Null()},
+ {{std::string("EXPANSION")}, std::string("expansion")},
+ {{std::string("\u212A")}, std::string("k")}, // UTF-8
shrinks from 3 to 1 byte.
+ {{std::string("")}, std::string("")}, // An empty-only run
between Unicode rows.
+ {{std::string("ΟΣ")},
+ std::string("ος")}, // Keep contextual final sigma
mapping.
+ {{std::string("你好A")}, std::string("你好a")}, // Consecutive
Unicode rows.
+ {{std::string("A\0Z", 3)}, std::string("a\0z", 3)},
+ {{std::string("TAIL")}, std::string("tail")},
+ {{std::string("")}, std::string("")},
+ });
+ upper_data.insert(
+ upper_data.end(),
+ {
+ {{std::string("\u0390")},
std::string("\u0399\u0308\u0301")}, // 2 to 6 bytes.
+ {{std::string("")}, std::string("")},
+ {{std::string("after")}, std::string("AFTER")},
+ {{Null()}, Null()},
+ {{std::string("expansion")}, std::string("EXPANSION")},
+ {{std::string("\u0131")}, std::string("I")}, // UTF-8
shrinks from 2 to 1 byte.
+ {{std::string("")}, std::string("")},
+ {{std::string("straße")}, std::string("STRASSE")},
+ {{std::string("你好a")}, std::string("你好A")},
+ {{std::string("a\0z", 3)}, std::string("A\0Z", 3)},
+ {{std::string("tail")}, std::string("TAIL")},
+ {{std::string("")}, std::string("")},
+ });
+ check_function_all_arg_comb<DataTypeString, true>("lower", input_types,
lower_data);
+ check_function_all_arg_comb<DataTypeString, true>("upper", input_types,
upper_data);
+}
+
// LOWER/UPPER/INITCAP case-map through the ICU root locale, independent of the
// process default locale. Under a Turkish (tr_TR) default locale ICU maps
ASCII
// 'I' -> 'ı' (U+0131) and 'i' -> 'İ' (U+0130); the root locale keeps the plain
// ASCII round-trip. This test installs a tr_TR ICU default and asserts every
// result stays locale independent.
//
-// TransferImpl/InitcapImpl only take the ICU path when the whole input buffer
-// contains a non-ASCII byte; pure-ASCII columns use the locale-neutral SIMD/C
-// fast path. Each data set therefore keeps a non-ASCII anchor row so that
-// check_function_all_arg_comb's batched (non-const) combination routes the
-// ASCII rows through ICU, where the default locale would otherwise leak in.
+// Mixed columns dispatch ASCII rows to the locale-neutral SIMD/C fast path
+// and non-ASCII rows to ICU. Each data set covers both paths in the batched
+// (non-const) combination and separately as constant arguments.
TEST(function_string_test, function_string_case_root_locale_test) {
UErrorCode status = U_ZERO_ERROR;
const icu::Locale saved_default = icu::Locale::getDefault();
@@ -772,7 +820,7 @@ TEST(function_string_test,
function_string_case_root_locale_test) {
DataSet data_set = {
{{std::string("I")}, std::string("i")}, // tr
default -> "ı"
{{std::string("KIZILAY")}, std::string("kizilay")}, // tr
default -> "kızılay"
- {{std::string("ÀÇ")}, std::string("àç")}, //
non-ASCII anchor: forces ICU
+ {{std::string("ÀÇ")}, std::string("àç")}, //
non-ASCII row: uses ICU
};
check_function_all_arg_comb<DataTypeString,
true>(std::string("lower"), input_types,
data_set);
@@ -2891,6 +2939,76 @@ TEST(function_string_test, function_parse_url_test) {
}
}
+TEST(function_string_test, function_parse_url_null_invalid_part_test) {
+ const InputTypeSet two_args = {PrimitiveType::TYPE_VARCHAR,
PrimitiveType::TYPE_VARCHAR};
+ const DataSet null_urls = {{{Null(), std::string("INVALID")}, Null()},
+ {{Null(), std::string("HOST")}, Null()}};
+ check_function_all_arg_comb<DataTypeString, true>("parse_url", two_args,
null_urls);
+
+ const InputTypeSet three_args = {PrimitiveType::TYPE_VARCHAR,
PrimitiveType::TYPE_VARCHAR,
+ PrimitiveType::TYPE_VARCHAR};
+ const DataSet null_inputs = {
+ {{Null(), std::string("INVALID"), std::string("k")}, Null()},
+ {{std::string("http://h/p?k=v"), std::string("INVALID"), Null()},
Null()}};
+ check_function_all_arg_comb<DataTypeString, true>("parse_url", three_args,
null_inputs);
+}
+
+TEST(function_string_test, function_translate_mixed_const_map_test) {
+ const auto check_batch = [](const std::string& to, const DataSet&
data_set) {
+ const auto type = make_nullable(std::make_shared<DataTypeString>());
+ const size_t row_count = data_set.size();
+ auto source = type->create_column();
+ auto expected = type->create_column();
+ for (const auto& row : data_set) {
+ ASSERT_TRUE(insert_cell(source, type, row.first[0]));
+ ASSERT_TRUE(insert_cell(expected, type, row.second));
+ }
+
+ // Keep mixed source rows in one batch, with singleton nested mapping
columns.
+ Block block;
+ block.insert({std::move(source), type, "source"});
+ block.insert(
+ {type->create_column_const(row_count,
Field(std::string("ab"))), type, "from"});
+ block.insert({type->create_column_const(row_count, Field(to)), type,
"to"});
+ auto function = SimpleFunctionFactory::instance().get_function(
+ "translate", block.get_columns_with_type_and_name(), type);
+ ASSERT_NE(function, nullptr);
+
+ FunctionUtils function_utils(type, {type, type, type}, false);
+ auto* context = function_utils.get_fn_ctx();
+ context->set_constant_cols(
+ {nullptr,
std::make_shared<ColumnPtrWrapper>(block.get_by_position(1).column),
+
std::make_shared<ColumnPtrWrapper>(block.get_by_position(2).column)});
+ ASSERT_TRUE(function->open(context,
FunctionContext::FRAGMENT_LOCAL).ok());
+ ASSERT_TRUE(function->open(context,
FunctionContext::THREAD_LOCAL).ok());
+ block.insert({nullptr, type, "result"});
+ ASSERT_TRUE(function->execute(context, block, {0, 1, 2}, 3,
row_count).ok());
+ ASSERT_TRUE(function->close(context,
FunctionContext::THREAD_LOCAL).ok());
+ ASSERT_TRUE(function->close(context,
FunctionContext::FRAGMENT_LOCAL).ok());
+
+ const auto& result = block.get_by_position(3).column;
+ ASSERT_EQ(row_count, result->size());
+ for (size_t row = 0; row < row_count; ++row) {
+ EXPECT_EQ(0, result->compare_at(row, row, *expected, 1))
+ << "row " << row << ", result: " <<
type->to_string(*result, row)
+ << ", expected: " << type->to_string(*expected, row);
+ }
+ };
+
+ const DataSet replace_rows = {{{std::string("aba")}, std::string("XYX")},
+ {{std::string("a你b好a")},
std::string("X你Y好X")},
+ {{std::string("")}, std::string("")},
+ {{Null()}, Null()}};
+ check_batch("XY", replace_rows);
+
+ const DataSet delete_rows = {{{std::string("aba")}, std::string("XX")},
+ {{std::string("a你b好a")}, std::string("X你好X")},
+ {{std::string("bbb")}, std::string("")},
+ {{std::string("")}, std::string("")},
+ {{Null()}, Null()}};
+ check_batch("X", delete_rows);
+}
+
TEST(function_string_test, function_hex_test) {
std::string func_name = "hex";
InputTypeSet input_types = {PrimitiveType::TYPE_VARCHAR};
diff --git a/be/test/util/simd/bits_test.cpp b/be/test/util/simd/bits_test.cpp
index cc85a92186f..abb8e4e340b 100644
--- a/be/test/util/simd/bits_test.cpp
+++ b/be/test/util/simd/bits_test.cpp
@@ -20,12 +20,79 @@
#include <gtest/gtest-message.h>
#include <gtest/gtest-test-part.h>
#include <gtest/gtest.h>
+#include <sys/mman.h>
+#include <unistd.h>
#include <algorithm>
#include "gtest/gtest_pred_impl.h"
namespace doris::simd {
+TEST(BitsTest, ContainsBytesGuardPages) {
+ const size_t page_size = sysconf(_SC_PAGESIZE);
+ void* mapping = mmap(nullptr, page_size * 3, PROT_NONE, MAP_PRIVATE |
MAP_ANONYMOUS, -1, 0);
+ ASSERT_NE(mapping, MAP_FAILED);
+ auto* page = static_cast<uint8_t*>(mapping) + page_size;
+ if (mprotect(page, page_size, PROT_READ | PROT_WRITE) != 0) {
+ munmap(mapping, page_size * 3);
+ FAIL() << "mprotect failed";
+ }
+ for (size_t size : {size_t(0), size_t(1), size_t(15), size_t(16),
size_t(17), size_t(31),
+ size_t(32), size_t(33), size_t(65), page_size}) {
+ for (auto* data : {page, page + page_size - size}) {
+ std::fill(data, data + size, 0);
+ EXPECT_FALSE(contain_one(data, size));
+ EXPECT_EQ(contain_zero(data, size), size != 0);
+ std::fill(data, data + size, 255);
+ EXPECT_EQ(contain_one(data, size), size != 0);
+ EXPECT_FALSE(contain_zero(data, size));
+ if (size != 0) {
+ for (size_t position : {size_t(0), size / 2, size - 1}) {
+ data[position] = 0;
+ EXPECT_TRUE(contain_zero(data, size));
+ data[position] = 255;
+ }
+ std::fill(data, data + size, 0);
+ for (size_t position : {size_t(0), size / 2, size - 1}) {
+ data[position] = 128;
+ EXPECT_TRUE(contain_one(data, size));
+ data[position] = 0;
+ }
+ }
+ }
+ }
+ EXPECT_EQ(munmap(mapping, page_size * 3), 0);
+}
+
+TEST(BitsTest, ContainsBytesAtEveryBoundary) {
+ EXPECT_FALSE(contain_one(nullptr, 0));
+ EXPECT_FALSE(contain_zero(nullptr, 0));
+ for (size_t size : {1U, 15U, 16U, 17U, 31U, 32U, 33U, 63U, 64U, 65U,
8160U}) {
+ for (size_t offset : {0U, 1U, 7U, 15U}) {
+ std::vector<uint8_t> storage(size + offset, 0);
+ auto* data = storage.data() + offset;
+ EXPECT_FALSE(contain_one(data, size));
+ EXPECT_TRUE(contain_zero(data, size));
+ for (uint8_t value : {1, 2, 128, 255}) {
+ std::fill(data, data + size, value);
+ EXPECT_TRUE(contain_one(data, size));
+ EXPECT_FALSE(contain_zero(data, size));
+ for (size_t position : {size_t(0), size / 2, size - 1}) {
+ data[position] = 0;
+ EXPECT_TRUE(contain_zero(data, size));
+ data[position] = value;
+ }
+ std::fill(data, data + size, 0);
+ for (size_t position : {size_t(0), size / 2, size - 1}) {
+ data[position] = value;
+ EXPECT_TRUE(contain_one(data, size));
+ data[position] = 0;
+ }
+ }
+ }
+ }
+}
+
TEST(BitsTest, BytesMaskToBitsMask) {
// Length determined by architecture (16 on NEON/aarch64, else 32)
constexpr auto len = bits_mask_length();
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]