Skip to content

perf(arrow): build repeated constant string/binary columns without per-row clones - #3080

Open
anoopj wants to merge 1 commit into
apache:mainfrom
anoopj:perf-repeated-array-alloc
Open

perf(arrow): build repeated constant string/binary columns without per-row clones#3080
anoopj wants to merge 1 commit into
apache:mainfrom
anoopj:perf-repeated-array-alloc

Conversation

@anoopj

@anoopj anoopj commented Aug 26, 2026

Copy link
Copy Markdown
Member

Which issue does this PR close?

Fixes #3079

What changes are included in this PR?

create_primitive_array_repeated built a throwaway vec![value.clone(); num_rows] for the Utf8/Binary/LargeBinary/FixedSizeBinary arms before handing it to the array constructor. For strings that clones the value into num_rows separate heap allocations per batch; the binary arms allocate a throwaway intermediate Vec.

Stream the single value straight into the Arrow buffer via from_iter_values with std::iter::repeat_n instead.

Benchmarks (release, throwaway harness, old vs new toggled on this file only):

  • leaf create_primitive_array_repeated (Utf8, 8192-row batch): 23.0 -> 4.1 ns/row (5.6x)
  • full scan select(x, _partition) over a real manifest + 262k-row parquet file, string-partitioned: 20.9 -> 5.7 ns/row (3.7x)

Note that the gain is allocation-counts and scales with how much of the projection is the string partition column.

Are these changes tested?

New + old tests

…r-row clones

create_primitive_array_repeated built a throwaway `vec![value.clone(); num_rows]`
for the Utf8/Binary/LargeBinary/FixedSizeBinary arms before handing it to the array
constructor. For strings that clones the value into num_rows separate heap
allocations per batch; the binary arms allocate a throwaway intermediate Vec.

Stream the single value straight into the Arrow buffer via `from_iter_values` with
`std::iter::repeat_n` instead.

Benchmarks (release, throwaway harness, old vs new toggled on this file only):
- leaf create_primitive_array_repeated (Utf8, 8192-row batch): 23.0 -> 4.1 ns/row (5.6x)
- full scan select(x, _partition) over a real manifest + 262k-row parquet file,
  string-partitioned: 20.9 -> 5.7 ns/row (3.7x)

Note that the gain is allocation-counts and scales with how much of the
projection is the string partition column.

Fixes apache#3079

@laskoviymishka laskoviymishka 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.

This is a nice!

One small non-blocking thing: the FixedSizeBinary arm. try_from_iter infers the fixed width from the data rather than the declared len, so an empty batch over a fixed[n] partition column looks like it'd come back typed FixedSizeBinary(0) and mismatch the declared schema. It's pre-existing, not something this PR introduced, but since the new empty-batch test already covers Utf8 it's a natural moment to extend it to fixed[n] and either prove it's fine or pin it down. Left the details inline.

One small description note: the Binary/LargeBinary arms weren't doing per-row allocations before — those were only replicating a &[u8] pointer, so the genuine per-row win is Utf8. Worth tightening the framing so the next person auditing this path isn't misled.

None of this blocks merge from my side.
Will soak for a day or two before merging

LargeBinaryArray::from_iter_values(std::iter::repeat_n(value.as_slice(), num_rows)),
),
(DataType::FixedSizeBinary(len), Some(PrimitiveLiteral::Binary(value))) => Arc::new(
FixedSizeBinaryArray::try_from_iter(std::iter::repeat_n(value.as_slice(), num_rows))

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.

I think there's a subtle edge worth nailing down while we're touching this arm. try_from_iter infers the fixed width from the data it's handed, not from the len we matched on — so with num_rows == 0 the iterator is empty and we get back a FixedSizeBinary(0) array no matter what the schema declared, and an empty batch over a fixed[n] partition column (any filter that drops all rows) would then hand a type-mismatched array to RecordBatch::try_new. Same story if a literal's length doesn't equal *lenlen only feeds the error string today, so a wrong-width value silently produces the wrong type rather than erroring.

Both are pre-existing rather than introduced here, but the new empty-batch test only covers Utf8. Extending test_create_string_array_repeated_empty to FixedSizeBinary(4) + num_rows=0 and asserting data_type() would either prove it's fine or pin it down, and a value.len() == *len guard before try_from_iter would close the wrong-width case. wdyt?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Repeated constant string/binary columns clone the value per row

2 participants