mirror of
https://github.com/duckdb/duckdb.git
synced 2026-10-28 08:46:33 +08:00
LPAD and RPAD: copy the padding from a per-call plan instead of decoding it character by character (#27858)
Fixes #27846. `InsertPadding` decoded one pad character per output character, flushing the pad whenever it ran out, so a 200-character pad cost 200 decodes and as many one-byte inserts per row. Since the decoder became an out-of-line call (#26485) and the result size check counted the pad again on every row (#26482), `lpad(x, 200, '0')` takes 1.75x as long on v2.0.0 as on v1.5.5 in the report, and about ten times as long as `repeat('0', 200) || x` on both. **The change** - The pad's geometry is counted once per call: its characters, the whole copies the padding needs, and the bytes of the remainder. The size check uses the same plan instead of counting again, the buffer is reserved to the result size, and the padding is copied whole, a single byte at a time for a one-byte pad. LEFT and RIGHT share one function. - Results and errors are unchanged: the existing tests pass, including the multibyte remainders and the maximum-size errors. **Measured** on an M-series Mac, one thread, 2M rows, seconds, best of 3: | statement | v1.5.5 | v2.0.0 | main | this PR | |---|---:|---:|---:|---:| | `lpad(i::VARCHAR, 18, '0')` | 0.131 | 0.151 | 0.145 | 0.098 | | `lpad(i::VARCHAR, 50, '0')` | 0.277 | 0.347 | 0.328 | 0.115 | | `lpad(i::VARCHAR, 200, '0')` | 0.984 | 1.227 | 1.114 | 0.126 | | `rpad(i::VARCHAR, 200, '0')` | 0.966 | 1.212 | 1.112 | 0.129 | | `lpad(i::VARCHAR, 200, 'ab😀')` | 0.979 | 1.313 | 1.264 | 0.628 | | `repeat('0', 200) \|\| i::VARCHAR` | 0.102 | 0.085 | 0.081 | 0.082 | **Verification** - `test/sql/function/string/test_pad.test` gains one-byte, one-character multibyte and longer-than-needed pads, a multibyte pad counted in characters, and the empty pad with and without padding needed; the string function group passes. - `benchmark/micro/string/lpad.benchmark`, registered for the regression job, tracks the one-byte case.
This commit is contained in:
4 files changed
+91
-72
No files matched your search
@@ -48,3 +48,4 @@ benchmark/micro/update/upsert_enum_key.benchmark
|
||||
benchmark/micro/aggregate/min_double.benchmark
|
||||
benchmark/micro/aggregate/max_double.benchmark
|
||||
benchmark/micro/pushdown/view_filter_below_delim_join.benchmark
|
||||
benchmark/micro/string/lpad.benchmark
|
||||
|
@@ -0,0 +1,12 @@
|
||||
# name: benchmark/micro/string/lpad.benchmark
|
||||
# description: LPAD of short strings to 200 characters with a one-byte pad
|
||||
# group: [string]
|
||||
|
||||
name LPAD
|
||||
group string
|
||||
|
||||
run
|
||||
SELECT max(strlen(lpad(i::VARCHAR, 200, '0'))) FROM range(2000000) t(i)
|
||||
|
||||
result I
|
||||
200
|
||||
@@ -22,21 +22,33 @@ static pair<idx_t, idx_t> PadCountChars(const idx_t len, const char *data, const
|
||||
return pair<idx_t, idx_t>(nbytes, nchars);
|
||||
}
|
||||
|
||||
static idx_t PaddingByteCount(const idx_t len, const string_t &pad) {
|
||||
// Count the bytes InsertPadding will write
|
||||
auto data = pad.GetData();
|
||||
auto size = pad.GetSize();
|
||||
if (len == 0 || size == 0) {
|
||||
return 0;
|
||||
//! The padding a call writes: whole copies of the pad, then the bytes of its first remainder characters. Decided from
|
||||
//! the pad's geometry, counted once per call, so that the output is copied rather than decoded character by character.
|
||||
struct PadPlan {
|
||||
idx_t pad_bytes = 0;
|
||||
idx_t pad_chars = 0;
|
||||
idx_t copies = 0;
|
||||
idx_t remainder_bytes = 0;
|
||||
|
||||
idx_t Bytes() const {
|
||||
return copies * pad_bytes + remainder_bytes;
|
||||
}
|
||||
auto pad_chars = PadCountChars(NumericLimits<idx_t>::Maximum(), data, size).second;
|
||||
auto remainder = PadCountChars(len % pad_chars, data, size).first;
|
||||
return (len / pad_chars) * size + remainder;
|
||||
};
|
||||
|
||||
static PadPlan PlanPadding(const idx_t len, const string_t &pad) {
|
||||
PadPlan plan;
|
||||
plan.pad_bytes = pad.GetSize();
|
||||
plan.pad_chars = PadCountChars(NumericLimits<idx_t>::Maximum(), pad.GetData(), plan.pad_bytes).second;
|
||||
if (len == 0 || plan.pad_chars == 0) {
|
||||
return plan;
|
||||
}
|
||||
plan.copies = len / plan.pad_chars;
|
||||
plan.remainder_bytes = PadCountChars(len % plan.pad_chars, pad.GetData(), plan.pad_bytes).first;
|
||||
return plan;
|
||||
}
|
||||
|
||||
static void CheckPadResultSize(const idx_t len, const string_t &pad, const pair<idx_t, idx_t> &written,
|
||||
const char *name) {
|
||||
auto result_size = written.first + PaddingByteCount(len - written.second, pad);
|
||||
static void CheckPadResultSize(const idx_t written_bytes, const PadPlan &plan, const char *name) {
|
||||
auto result_size = written_bytes + plan.Bytes();
|
||||
if (result_size > string_t::MAX_STRING_SIZE) {
|
||||
throw OutOfRangeException(
|
||||
"Cannot create a string of size: '%d' in %s, the maximum supported string size is: '%d'", result_size, name,
|
||||
@@ -44,37 +56,21 @@ static void CheckPadResultSize(const idx_t len, const string_t &pad, const pair<
|
||||
}
|
||||
}
|
||||
|
||||
static bool InsertPadding(const idx_t len, const string_t &pad, vector<char> &result) {
|
||||
// Copy the padding until the output is long enough
|
||||
static void InsertPadding(const string_t &pad, const PadPlan &plan, vector<char> &result) {
|
||||
auto data = pad.GetData();
|
||||
auto size = pad.GetSize();
|
||||
|
||||
// Check whether we need data that we don't have
|
||||
if (len > 0 && size == 0) {
|
||||
return false;
|
||||
if (plan.pad_bytes == 1) {
|
||||
result.insert(result.end(), plan.copies, data[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Insert characters until we have all we need.
|
||||
idx_t nbytes = 0;
|
||||
for (idx_t nchars = 0; nchars < len; ++nchars) {
|
||||
// If we are at the end of the pad, flush all of it and loop back
|
||||
if (nbytes >= size) {
|
||||
result.insert(result.end(), data, data + size);
|
||||
nbytes = 0;
|
||||
}
|
||||
|
||||
// Write the next character
|
||||
int32_t codepoint;
|
||||
nbytes += DecodeCodepoint(data + nbytes, size - nbytes, codepoint);
|
||||
for (idx_t i = 0; i < plan.copies; i++) {
|
||||
result.insert(result.end(), data, data + plan.pad_bytes);
|
||||
}
|
||||
|
||||
// Flush the remaining pad
|
||||
result.insert(result.end(), data, data + nbytes);
|
||||
|
||||
return true;
|
||||
result.insert(result.end(), data, data + plan.remainder_bytes);
|
||||
}
|
||||
|
||||
static string_t LeftPadFunction(const string_t &str, const int32_t len, const string_t &pad, vector<char> &result) {
|
||||
template <bool LEFT>
|
||||
static string_t PadString(const string_t &str, const int32_t len, const string_t &pad, vector<char> &result,
|
||||
const char *name) {
|
||||
// Reuse the buffer
|
||||
result.clear();
|
||||
|
||||
@@ -82,55 +78,37 @@ static string_t LeftPadFunction(const string_t &str, const int32_t len, const st
|
||||
auto data_str = str.GetData();
|
||||
auto size_str = str.GetSize();
|
||||
|
||||
// Count how much of str will fit in the output
|
||||
// Count how much of str will fit in the output, and pad by the number of characters still needed
|
||||
auto written = PadCountChars(UnsafeNumericCast<idx_t>(len), data_str, size_str);
|
||||
CheckPadResultSize(UnsafeNumericCast<idx_t>(len), pad, written, "LPAD");
|
||||
|
||||
// Left pad by the number of characters still needed
|
||||
if (!InsertPadding(UnsafeNumericCast<idx_t>(len) - written.second, pad, result)) {
|
||||
throw InvalidInputException("Insufficient padding in LPAD.");
|
||||
auto padding_chars = UnsafeNumericCast<idx_t>(len) - written.second;
|
||||
auto plan = PlanPadding(padding_chars, pad);
|
||||
CheckPadResultSize(written.first, plan, name);
|
||||
if (padding_chars > 0 && plan.pad_chars == 0) {
|
||||
throw InvalidInputException("Insufficient padding in %s.", name);
|
||||
}
|
||||
result.reserve(written.first + plan.Bytes());
|
||||
|
||||
// Append as much of the original string as fits
|
||||
result.insert(result.end(), data_str, data_str + written.first);
|
||||
|
||||
if (LEFT) {
|
||||
InsertPadding(pad, plan, result);
|
||||
result.insert(result.end(), data_str, data_str + written.first);
|
||||
} else {
|
||||
result.insert(result.end(), data_str, data_str + written.first);
|
||||
InsertPadding(pad, plan, result);
|
||||
}
|
||||
return string_t(result.data(), UnsafeNumericCast<uint32_t>(result.size()));
|
||||
}
|
||||
|
||||
struct LeftPadOperator {
|
||||
static inline string_t Operation(const string_t &str, const int32_t len, const string_t &pad,
|
||||
vector<char> &result) {
|
||||
return LeftPadFunction(str, len, pad, result);
|
||||
return PadString<true>(str, len, pad, result, "LPAD");
|
||||
}
|
||||
};
|
||||
|
||||
static string_t RightPadFunction(const string_t &str, const int32_t len, const string_t &pad, vector<char> &result) {
|
||||
// Reuse the buffer
|
||||
result.clear();
|
||||
|
||||
// Get information about the base string
|
||||
auto data_str = str.GetData();
|
||||
auto size_str = str.GetSize();
|
||||
|
||||
// Count how much of str will fit in the output
|
||||
auto written = PadCountChars(UnsafeNumericCast<idx_t>(len), data_str, size_str);
|
||||
CheckPadResultSize(UnsafeNumericCast<idx_t>(len), pad, written, "RPAD");
|
||||
|
||||
// Append as much of the original string as fits
|
||||
result.insert(result.end(), data_str, data_str + written.first);
|
||||
|
||||
// Right pad by the number of characters still needed
|
||||
if (!InsertPadding(UnsafeNumericCast<idx_t>(len) - written.second, pad, result)) {
|
||||
throw InvalidInputException("Insufficient padding in RPAD.");
|
||||
};
|
||||
|
||||
return string_t(result.data(), UnsafeNumericCast<uint32_t>(result.size()));
|
||||
}
|
||||
|
||||
struct RightPadOperator {
|
||||
static inline string_t Operation(const string_t &str, const int32_t len, const string_t &pad,
|
||||
vector<char> &result) {
|
||||
return RightPadFunction(str, len, pad, result);
|
||||
return PadString<false>(str, len, pad, result, "RPAD");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -142,3 +142,31 @@ query I
|
||||
select RPAD('ab', 7, 'xy😀')
|
||||
----
|
||||
abxy😀xy
|
||||
|
||||
# a one-byte pad, a one-character multibyte pad, and a pad longer than the padding
|
||||
query IIII
|
||||
select LPAD('x', 10, '0'), LPAD('x', 4, '😀'), LPAD('x', 3, '😀ab'), RPAD('x', 3, '😀ab')
|
||||
----
|
||||
000000000x 😀😀😀x 😀ax x😀a
|
||||
|
||||
# the padding is counted in characters whatever the byte width of the pad
|
||||
query III
|
||||
select length(LPAD('', 1000, 'é')), strlen(LPAD('', 1000, 'é')), LPAD('ab', 5, 'é') = 'éééab'
|
||||
----
|
||||
1000 2000 true
|
||||
|
||||
# no padding needed: the pad is not read
|
||||
query II
|
||||
select LPAD('abc', 3, ''), RPAD('abcd', 3, '')
|
||||
----
|
||||
abc abc
|
||||
|
||||
statement error
|
||||
select LPAD('abc', 4, '')
|
||||
----
|
||||
Insufficient padding in LPAD
|
||||
|
||||
statement error
|
||||
select RPAD('abc', 4, '')
|
||||
----
|
||||
Insufficient padding in RPAD
|
||||
Reference in new issue
Block a user