This is an automated email from the ASF dual-hosted git repository.

alamb pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion.git


The following commit(s) were added to refs/heads/main by this push:
     new 94034bef93  Improve lpad udf by using a GenericStringBuilder (#11987)
94034bef93 is described below

commit 94034bef9357b166d5593de90b00a35563eec3f8
Author: Bruce Ritchie <[email protected]>
AuthorDate: Wed Aug 14 17:11:33 2024 -0400

     Improve lpad udf by using a GenericStringBuilder (#11987)
    
    * Update LPAD scalar function to support Utf8View
    
    * Lpad code improvements and benchmark.
    
    * Improved use of GenericStringBuilder.
---
 datafusion/functions/Cargo.toml          |   5 +
 datafusion/functions/benches/pad.rs      | 141 ++++++++++++++++++++
 datafusion/functions/src/unicode/lpad.rs | 213 ++++++++++++++-----------------
 3 files changed, 244 insertions(+), 115 deletions(-)

diff --git a/datafusion/functions/Cargo.toml b/datafusion/functions/Cargo.toml
index 9675d03a01..688563baec 100644
--- a/datafusion/functions/Cargo.toml
+++ b/datafusion/functions/Cargo.toml
@@ -146,3 +146,8 @@ required-features = ["string_expressions"]
 harness = false
 name = "upper"
 required-features = ["string_expressions"]
+
+[[bench]]
+harness = false
+name = "pad"
+required-features = ["unicode_expressions"]
diff --git a/datafusion/functions/benches/pad.rs 
b/datafusion/functions/benches/pad.rs
new file mode 100644
index 0000000000..5ff1e2fb86
--- /dev/null
+++ b/datafusion/functions/benches/pad.rs
@@ -0,0 +1,141 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use arrow::array::{ArrayRef, ArrowPrimitiveType, OffsetSizeTrait, 
PrimitiveArray};
+use arrow::datatypes::Int64Type;
+use arrow::util::bench_util::{
+    create_string_array_with_len, create_string_view_array_with_len,
+};
+use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
+use datafusion_expr::ColumnarValue;
+use datafusion_functions::unicode::{lpad, rpad};
+use rand::distributions::{Distribution, Uniform};
+use rand::Rng;
+use std::sync::Arc;
+
+struct Filter<Dist> {
+    dist: Dist,
+}
+
+impl<T, Dist> Distribution<T> for Filter<Dist>
+where
+    Dist: Distribution<T>,
+{
+    fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> T {
+        self.dist.sample(rng)
+    }
+}
+
+pub fn create_primitive_array<T>(
+    size: usize,
+    null_density: f32,
+    len: usize,
+) -> PrimitiveArray<T>
+where
+    T: ArrowPrimitiveType<Native = i64>,
+{
+    let dist = Filter {
+        dist: Uniform::new_inclusive::<i64, i64>(0, len as i64),
+    };
+
+    let mut rng = rand::thread_rng();
+    (0..size)
+        .map(|_| {
+            if rng.gen::<f32>() < null_density {
+                None
+            } else {
+                Some(rng.sample(&dist))
+            }
+        })
+        .collect()
+}
+
+fn create_args<O: OffsetSizeTrait>(
+    size: usize,
+    str_len: usize,
+    use_string_view: bool,
+) -> Vec<ColumnarValue> {
+    let length_array = Arc::new(create_primitive_array::<Int64Type>(size, 0.0, 
str_len));
+
+    if !use_string_view {
+        let string_array =
+            Arc::new(create_string_array_with_len::<O>(size, 0.1, str_len));
+        let fill_array = Arc::new(create_string_array_with_len::<O>(size, 0.1, 
str_len));
+
+        vec![
+            ColumnarValue::Array(string_array),
+            ColumnarValue::Array(Arc::clone(&length_array) as ArrayRef),
+            ColumnarValue::Array(fill_array),
+        ]
+    } else {
+        let string_array =
+            Arc::new(create_string_view_array_with_len(size, 0.1, str_len, 
false));
+        let fill_array =
+            Arc::new(create_string_view_array_with_len(size, 0.1, str_len, 
false));
+
+        vec![
+            ColumnarValue::Array(string_array),
+            ColumnarValue::Array(Arc::clone(&length_array) as ArrayRef),
+            ColumnarValue::Array(fill_array),
+        ]
+    }
+}
+
+fn criterion_benchmark(c: &mut Criterion) {
+    for size in [1024, 2048] {
+        let mut group = c.benchmark_group("lpad function");
+
+        let args = create_args::<i32>(size, 32, false);
+        group.bench_function(BenchmarkId::new("utf8 type", size), |b| {
+            b.iter(|| criterion::black_box(lpad().invoke(&args).unwrap()))
+        });
+
+        let args = create_args::<i64>(size, 32, false);
+        group.bench_function(BenchmarkId::new("largeutf8 type", size), |b| {
+            b.iter(|| criterion::black_box(lpad().invoke(&args).unwrap()))
+        });
+
+        let args = create_args::<i32>(size, 32, true);
+        group.bench_function(BenchmarkId::new("stringview type", size), |b| {
+            b.iter(|| criterion::black_box(lpad().invoke(&args).unwrap()))
+        });
+
+        group.finish();
+
+        let mut group = c.benchmark_group("rpad function");
+
+        let args = create_args::<i32>(size, 32, false);
+        group.bench_function(BenchmarkId::new("utf8 type", size), |b| {
+            b.iter(|| criterion::black_box(rpad().invoke(&args).unwrap()))
+        });
+
+        let args = create_args::<i64>(size, 32, false);
+        group.bench_function(BenchmarkId::new("largeutf8 type", size), |b| {
+            b.iter(|| criterion::black_box(rpad().invoke(&args).unwrap()))
+        });
+        //
+        // let args = create_args::<i32>(size, 32, true);
+        // group.bench_function(BenchmarkId::new("stringview type", size), |b| 
{
+        //     b.iter(|| criterion::black_box(rpad().invoke(&args).unwrap()))
+        // });
+
+        group.finish();
+    }
+}
+
+criterion_group!(benches, criterion_benchmark);
+criterion_main!(benches);
diff --git a/datafusion/functions/src/unicode/lpad.rs 
b/datafusion/functions/src/unicode/lpad.rs
index 5caa6acd67..521cdc5d0f 100644
--- a/datafusion/functions/src/unicode/lpad.rs
+++ b/datafusion/functions/src/unicode/lpad.rs
@@ -16,11 +16,12 @@
 // under the License.
 
 use std::any::Any;
+use std::fmt::Write;
 use std::sync::Arc;
 
 use arrow::array::{
-    Array, ArrayAccessor, ArrayIter, ArrayRef, AsArray, GenericStringArray, 
Int64Array,
-    OffsetSizeTrait, StringViewArray,
+    Array, ArrayAccessor, ArrayIter, ArrayRef, AsArray, GenericStringArray,
+    GenericStringBuilder, Int64Array, OffsetSizeTrait, StringViewArray,
 };
 use arrow::datatypes::DataType;
 use unicode_segmentation::UnicodeSegmentation;
@@ -87,14 +88,18 @@ impl ScalarUDFImpl for LPadFunc {
     }
 
     fn invoke(&self, args: &[ColumnarValue]) -> Result<ColumnarValue> {
-        make_scalar_function(lpad, vec![])(args)
+        match args[0].data_type() {
+            Utf8 | Utf8View => make_scalar_function(lpad::<i32>, vec![])(args),
+            LargeUtf8 => make_scalar_function(lpad::<i64>, vec![])(args),
+            other => exec_err!("Unsupported data type {other:?} for function 
lpad"),
+        }
     }
 }
 
 /// Extends the string to length 'length' by prepending the characters fill (a 
space by default).
 /// If the string is already longer than length then it is truncated (on the 
right).
 /// lpad('hi', 5, 'xy') = 'xyxhi'
-pub fn lpad(args: &[ArrayRef]) -> Result<ArrayRef> {
+pub fn lpad<T: OffsetSizeTrait>(args: &[ArrayRef]) -> Result<ArrayRef> {
     if args.len() <= 1 || args.len() > 3 {
         return exec_err!(
             "lpad was called with {} arguments. It requires at least 2 and at 
most 3.",
@@ -104,49 +109,28 @@ pub fn lpad(args: &[ArrayRef]) -> Result<ArrayRef> {
 
     let length_array = as_int64_array(&args[1])?;
 
-    match args[0].data_type() {
-        Utf8 => match args.len() {
-            2 => lpad_impl::<&GenericStringArray<i32>, 
&GenericStringArray<i32>, i32>(
-                args[0].as_string::<i32>(),
-                length_array,
-                None,
-            ),
-            3 => lpad_with_replace::<&GenericStringArray<i32>, i32>(
-                args[0].as_string::<i32>(),
-                length_array,
-                &args[2],
-            ),
-            _ => unreachable!(),
-        },
-        LargeUtf8 => match args.len() {
-            2 => lpad_impl::<&GenericStringArray<i64>, 
&GenericStringArray<i64>, i64>(
-                args[0].as_string::<i64>(),
-                length_array,
-                None,
-            ),
-            3 => lpad_with_replace::<&GenericStringArray<i64>, i64>(
-                args[0].as_string::<i64>(),
-                length_array,
-                &args[2],
-            ),
-            _ => unreachable!(),
-        },
-        Utf8View => match args.len() {
-            2 => lpad_impl::<&StringViewArray, &GenericStringArray<i32>, i32>(
-                args[0].as_string_view(),
-                length_array,
-                None,
-            ),
-            3 => lpad_with_replace::<&StringViewArray, i32>(
-                args[0].as_string_view(),
-                length_array,
-                &args[2],
-            ),
-            _ => unreachable!(),
-        },
-        other => {
-            exec_err!("Unsupported data type {other:?} for function lpad")
-        }
+    match (args.len(), args[0].data_type()) {
+        (2, Utf8View) => lpad_impl::<&StringViewArray, 
&GenericStringArray<i32>, T>(
+            args[0].as_string_view(),
+            length_array,
+            None,
+        ),
+        (2, Utf8 | LargeUtf8) => lpad_impl::<
+            &GenericStringArray<T>,
+            &GenericStringArray<T>,
+            T,
+        >(args[0].as_string::<T>(), length_array, None),
+        (3, Utf8View) => lpad_with_replace::<&StringViewArray, T>(
+            args[0].as_string_view(),
+            length_array,
+            &args[2],
+        ),
+        (3, Utf8 | LargeUtf8) => lpad_with_replace::<&GenericStringArray<T>, 
T>(
+            args[0].as_string::<T>(),
+            length_array,
+            &args[2],
+        ),
+        (_, _) => unreachable!(),
     }
 }
 
@@ -159,20 +143,20 @@ where
     V: StringArrayType<'a>,
 {
     match fill_array.data_type() {
-        Utf8 => lpad_impl::<V, &GenericStringArray<i32>, T>(
+        Utf8View => lpad_impl::<V, &StringViewArray, T>(
             string_array,
             length_array,
-            Some(fill_array.as_string::<i32>()),
+            Some(fill_array.as_string_view()),
         ),
         LargeUtf8 => lpad_impl::<V, &GenericStringArray<i64>, T>(
             string_array,
             length_array,
             Some(fill_array.as_string::<i64>()),
         ),
-        Utf8View => lpad_impl::<V, &StringViewArray, T>(
+        Utf8 => lpad_impl::<V, &GenericStringArray<i32>, T>(
             string_array,
             length_array,
-            Some(fill_array.as_string_view()),
+            Some(fill_array.as_string::<i32>()),
         ),
         other => {
             exec_err!("Unsupported data type {other:?} for function lpad")
@@ -190,87 +174,86 @@ where
     V2: StringArrayType<'a>,
     T: OffsetSizeTrait,
 {
-    if fill_array.is_none() {
-        let result = string_array
-            .iter()
-            .zip(length_array.iter())
-            .map(|(string, length)| match (string, length) {
-                (Some(string), Some(length)) => {
-                    if length > i32::MAX as i64 {
-                        return exec_err!("lpad requested length {length} too 
large");
-                    }
+    let array = if fill_array.is_none() {
+        let mut builder: GenericStringBuilder<T> = GenericStringBuilder::new();
 
-                    let length = if length < 0 { 0 } else { length as usize };
-                    if length == 0 {
-                        Ok(Some("".to_string()))
-                    } else {
-                        let graphemes = 
string.graphemes(true).collect::<Vec<&str>>();
-                        if length < graphemes.len() {
-                            Ok(Some(graphemes[..length].concat()))
-                        } else {
-                            let mut s: String = " ".repeat(length - 
graphemes.len());
-                            s.push_str(string);
-                            Ok(Some(s))
-                        }
-                    }
+        for (string, length) in string_array.iter().zip(length_array.iter()) {
+            if let (Some(string), Some(length)) = (string, length) {
+                if length > i32::MAX as i64 {
+                    return exec_err!("lpad requested length {length} too 
large");
                 }
-                _ => Ok(None),
-            })
-            .collect::<Result<GenericStringArray<T>>>()?;
 
-        Ok(Arc::new(result) as ArrayRef)
+                let length = if length < 0 { 0 } else { length as usize };
+                if length == 0 {
+                    builder.append_value("");
+                    continue;
+                }
+
+                let graphemes = string.graphemes(true).collect::<Vec<&str>>();
+                if length < graphemes.len() {
+                    builder.append_value(graphemes[..length].concat());
+                } else {
+                    builder.write_str(" ".repeat(length - 
graphemes.len()).as_str())?;
+                    builder.write_str(string)?;
+                    builder.append_value("");
+                }
+            } else {
+                builder.append_null();
+            }
+        }
+
+        builder.finish()
     } else {
-        let result = string_array
+        let mut builder: GenericStringBuilder<T> = GenericStringBuilder::new();
+
+        for ((string, length), fill) in string_array
             .iter()
             .zip(length_array.iter())
             .zip(fill_array.unwrap().iter())
-            .map(|((string, length), fill)| match (string, length, fill) {
-                (Some(string), Some(length), Some(fill)) => {
-                    if length > i32::MAX as i64 {
-                        return exec_err!("lpad requested length {length} too 
large");
-                    }
+        {
+            if let (Some(string), Some(length), Some(fill)) = (string, length, 
fill) {
+                if length > i32::MAX as i64 {
+                    return exec_err!("lpad requested length {length} too 
large");
+                }
 
-                    let length = if length < 0 { 0 } else { length as usize };
-                    if length == 0 {
-                        Ok(Some("".to_string()))
-                    } else {
-                        let graphemes = 
string.graphemes(true).collect::<Vec<&str>>();
-                        let fill_chars = fill.chars().collect::<Vec<char>>();
-
-                        if length < graphemes.len() {
-                            Ok(Some(graphemes[..length].concat()))
-                        } else if fill_chars.is_empty() {
-                            Ok(Some(string.to_string()))
-                        } else {
-                            let mut s = string.to_string();
-                            let mut char_vector =
-                                Vec::<char>::with_capacity(length - 
graphemes.len());
-                            for l in 0..length - graphemes.len() {
-                                char_vector
-                                    .push(*fill_chars.get(l % 
fill_chars.len()).unwrap());
-                            }
-                            s.insert_str(
-                                0,
-                                
char_vector.iter().collect::<String>().as_str(),
-                            );
-                            Ok(Some(s))
-                        }
+                let length = if length < 0 { 0 } else { length as usize };
+                if length == 0 {
+                    builder.append_value("");
+                    continue;
+                }
+
+                let graphemes = string.graphemes(true).collect::<Vec<&str>>();
+                let fill_chars = fill.chars().collect::<Vec<char>>();
+
+                if length < graphemes.len() {
+                    builder.append_value(graphemes[..length].concat());
+                } else if fill_chars.is_empty() {
+                    builder.append_value(string);
+                } else {
+                    for l in 0..length - graphemes.len() {
+                        let c = *fill_chars.get(l % fill_chars.len()).unwrap();
+                        builder.write_char(c)?;
                     }
+                    builder.write_str(string)?;
+                    builder.append_value("");
                 }
-                _ => Ok(None),
-            })
-            .collect::<Result<GenericStringArray<T>>>()?;
+            } else {
+                builder.append_null();
+            }
+        }
 
-        Ok(Arc::new(result) as ArrayRef)
-    }
+        builder.finish()
+    };
+
+    Ok(Arc::new(array) as ArrayRef)
 }
 
 trait StringArrayType<'a>: ArrayAccessor<Item = &'a str> + Sized {
     fn iter(&self) -> ArrayIter<Self>;
 }
-impl<'a, O: OffsetSizeTrait> StringArrayType<'a> for &'a GenericStringArray<O> 
{
+impl<'a, T: OffsetSizeTrait> StringArrayType<'a> for &'a GenericStringArray<T> 
{
     fn iter(&self) -> ArrayIter<Self> {
-        GenericStringArray::<O>::iter(self)
+        GenericStringArray::<T>::iter(self)
     }
 }
 impl<'a> StringArrayType<'a> for &'a StringViewArray {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to