Skip to content

perf: Optimize lpad(), rpad() for scalar args - #20657

Merged
timsaucer merged 7 commits into
apache:mainfrom
neilconway:neilc/optimize-pad-scalars
Mar 20, 2026
Merged

timsaucer merged 7 commits into
apache:mainfrom
neilconway:neilc/optimize-pad-scalars

Conversation

@neilconway

@neilconway neilconway commented Mar 2, 2026 •

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Rationale for this change

lpad and rpad are commonly called with scalar length and fill arguments, e.g. lpad(column, 20, '0'). We can special-case this scenario to improve performance by avoiding the overhead of make_scalar_function and by precomputing the padding buffer and reusing it for each row.

For scalar args, this improves performance by ~3x for ASCII inputs and ~1.6x for Unicode inputs.

What changes are included in this PR?

  • Add benchmarks for padding with scalar length and fill.
  • Add a scalar fast path for lpad and rpad that precomputes a padding buffer. We only use the fast path if the pad length is reasonably small (<= 16KB), to avoid using too much memory on a scratch buffer.
  • Code cleanup: extract and use try_as_scalar_str and try_as_scalar_i64 helpers.
  • Code cleanup: use target_len consistently instead of length, because the latter is ambiguous.
  • Code cleanup: make rpad and lpad more similar by removing needless and probably unintended differences between the two functions. We could go further and refactor them to remove the redundancy but I won't attempt that for now.

Are these changes tested?

Yes; covered by existing tests. Added new benchmarks.

Are there any user-facing changes?

No.

AI usage

Multiple AI tools were used to iterate on this PR. I have reviewed and understand the resulting code.

@github-actions github-actions Bot added the functions Changes to functions implementation label Mar 2, 2026
@neilconway

Copy link
Copy Markdown
Contributor Author

Benchmarks:

⏺ ┌───────────────────────────────────────────┬──────────┬───────────┬────────┐
  │                 Benchmark                 │   Main   │ Optimized │ Change │
  ├───────────────────────────────────────────┼──────────┼───────────┼────────┤
  │ lpad utf8 scalar (1024, fill='x')         │ 27.5 µs  │ 8.3 µs    │ -70%   │
  ├───────────────────────────────────────────┼──────────┼───────────┼────────┤
  │ lpad stringview scalar (1024, fill='x')   │ 27.6 µs  │ 9.7 µs    │ -65%   │
  ├───────────────────────────────────────────┼──────────┼───────────┼────────┤
  │ lpad utf8 scalar unicode (1024, fill='é') │ 74.3 µs  │ 44.1 µs   │ -41%   │
  ├───────────────────────────────────────────┼──────────┼───────────┼────────┤
  │ lpad utf8 scalar (4096, fill='x')         │ 109.0 µs │ 33.1 µs   │ -70%   │
  ├───────────────────────────────────────────┼──────────┼───────────┼────────┤
  │ lpad stringview scalar (4096, fill='x')   │ 109.5 µs │ 39.6 µs   │ -64%   │
  ├───────────────────────────────────────────┼──────────┼───────────┼────────┤
  │ lpad utf8 scalar unicode (4096, fill='é') │ 296.6 µs │ 173.2 µs  │ -42%   │
  ├───────────────────────────────────────────┼──────────┼───────────┼────────┤
  │ rpad utf8 scalar (1024, fill='x')         │ 30.5 µs  │ 10.6 µs   │ -65%   │
  ├───────────────────────────────────────────┼──────────┼───────────┼────────┤
  │ rpad stringview scalar (1024, fill='x')   │ 29.0 µs  │ 11.1 µs   │ -62%   │
  ├───────────────────────────────────────────┼──────────┼───────────┼────────┤
  │ rpad utf8 scalar unicode (1024, fill='é') │ 78.9 µs  │ 46.3 µs   │ -41%   │
  ├───────────────────────────────────────────┼──────────┼───────────┼────────┤
  │ rpad utf8 scalar (4096, fill='x')         │ 124.2 µs │ 43.6 µs   │ -65%   │
  ├───────────────────────────────────────────┼──────────┼───────────┼────────┤
  │ rpad stringview scalar (4096, fill='x')   │ 114.8 µs │ 44.6 µs   │ -61%   │
  ├───────────────────────────────────────────┼──────────┼───────────┼────────┤
  │ rpad utf8 scalar unicode (4096, fill='é') │ 313.5 µs │ 182.7 µs  │ -42%   │
  └───────────────────────────────────────────┴──────────┴───────────┴────────┘

@neilconway

Copy link
Copy Markdown
Contributor Author

Commenting to bring this back to the top of the Pending PRs list 😊

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@martin-g
martin-g self-requested a review March 18, 2026 21:04
…alars

# Conflicts:
#	datafusion/functions/src/unicode/lpad.rs
#	datafusion/functions/src/unicode/rpad.rs
@neilconway

Copy link
Copy Markdown
Contributor Author

BTW, I debated whether it is actually worth the trouble to cap the padding buffer's size. For small inputs with very large target lengths, the padding buffer's relative memory use will be high, but it should be fine in most other cases. If anyone has a view, lmk.

@neilconway neilconway changed the title perf: Optimize lpad, rpad for scalar args perf: Optimize lpad(), rpad() for scalar args Mar 19, 2026

@timsaucer timsaucer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for another excellent PR!

@timsaucer
timsaucer added this pull request to the merge queue Mar 20, 2026
Merged via the queue into apache:main with commit 5e54b89 Mar 20, 2026
31 checks passed
haohuaijin pushed a commit to haohuaijin/arrow-datafusion that referenced this pull request Mar 21, 2026
## Which issue does this PR close?

- Closes apache#20655.

## Rationale for this change

`lpad` and `rpad` are commonly called with scalar length and fill
arguments, e.g. `lpad(column, 20, '0')`. We can special-case this
scenario to improve performance by avoiding the overhead of
`make_scalar_function` and by precomputing the padding buffer and
reusing it for each row.

For scalar args, this improves performance by ~3x for ASCII inputs and
~1.6x for Unicode inputs.

## What changes are included in this PR?

- Add benchmarks for padding with scalar length and fill.
- Add a scalar fast path for `lpad` and `rpad` that precomputes a
padding buffer. We only use the fast path if the pad length is
reasonably small (<= 16KB), to avoid using too much memory on a scratch
buffer.
- Code cleanup: extract and use `try_as_scalar_str` and
`try_as_scalar_i64` helpers.
- Code cleanup: use `target_len` consistently instead of `length`,
because the latter is ambiguous.
- Code cleanup: make `rpad` and `lpad` more similar by removing needless
and probably unintended differences between the two functions. We could
go further and refactor them to remove the redundancy but I won't
attempt that for now.

## Are these changes tested?

Yes; covered by existing tests. Added new benchmarks.

## Are there any user-facing changes?

No.

## AI usage

Multiple AI tools were used to iterate on this PR. I have reviewed and
understand the resulting code.
@neilconway
neilconway deleted the neilc/optimize-pad-scalars branch March 21, 2026 15:10
de-bgunter pushed a commit to de-bgunter/datafusion that referenced this pull request Mar 24, 2026
## Which issue does this PR close?

- Closes apache#20655.

## Rationale for this change

`lpad` and `rpad` are commonly called with scalar length and fill
arguments, e.g. `lpad(column, 20, '0')`. We can special-case this
scenario to improve performance by avoiding the overhead of
`make_scalar_function` and by precomputing the padding buffer and
reusing it for each row.

For scalar args, this improves performance by ~3x for ASCII inputs and
~1.6x for Unicode inputs.

## What changes are included in this PR?

- Add benchmarks for padding with scalar length and fill.
- Add a scalar fast path for `lpad` and `rpad` that precomputes a
padding buffer. We only use the fast path if the pad length is
reasonably small (<= 16KB), to avoid using too much memory on a scratch
buffer.
- Code cleanup: extract and use `try_as_scalar_str` and
`try_as_scalar_i64` helpers.
- Code cleanup: use `target_len` consistently instead of `length`,
because the latter is ambiguous.
- Code cleanup: make `rpad` and `lpad` more similar by removing needless
and probably unintended differences between the two functions. We could
go further and refactor them to remove the redundancy but I won't
attempt that for now.

## Are these changes tested?

Yes; covered by existing tests. Added new benchmarks.

## Are there any user-facing changes?

No.

## AI usage

Multiple AI tools were used to iterate on this PR. I have reviewed and
understand the resulting code.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

functions Changes to functions implementation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Optimize lpad, rpad for scalar args

4 participants