Skip to content

WIP: Adding support for contrib/intarray style fast-allocated arrays#2086

Closed
mhov wants to merge 7 commits into
pgcentralfoundation:developfrom
mhov:array-alloc
Closed

WIP: Adding support for contrib/intarray style fast-allocated arrays#2086
mhov wants to merge 7 commits into
pgcentralfoundation:developfrom
mhov:array-alloc

Conversation

@mhov

@mhov mhov commented Jun 24, 2025

Copy link
Copy Markdown
Contributor

Faster arrays!

Now that Array<T> is stable, this PR ports in performance enhancements for creating PostgreSQL int[] arrays—based on the optimizations found in PostgreSQL’s contrib/intarray extension (specifically new_intArrayType(int num)).

The goal is to significantly speed up the conversion from &[i32] to a PostgreSQL ArrayType Datum.

Background

Currently, to return a PostgreSQL int[] from a Rust function, we typically return a Vec<i32>, which then gets converted to a Datum via array_datum_from_iter(..)
(implementation here).

This approach uses:

  • pg_sys::initArrayResult
  • pg_sys::accumArrayResult
  • pg_sys::makeArrayResult

These APIs build a varlena array by accumulating values and repeatedly reallocating memory as the capacity grows. For larger arrays, this results in substantial performance overhead.

Optimization

PostgreSQL's contrib/intarray takes a different approach for fixed-size datums: it pre-allocates the entire array up front and directly memcpys the data into ARR_DATA_PTR. This avoids the need for reallocations and is much faster.

What I've added

  • Implemented BoxRet for Array<'a, T> when T is a fixed-size numeric type (i8, i16, i32, f32, f64)
    • Enables using Array<T> directly as a return type from #[pg_extern] functions.
  • Adds optimized allocation logic for these numeric types
    • Pre-allocates the full ArrayType
    • Uses memcpy-like behavior for fast construction from slices
  • Adds helper functions for:
    • Creating empty arrays
    • Creating Array<T> from &[T]

Benchmarks

I've benchmarked two approaches for turning Vec<i32> -> Array<i32>:

  1. The current array_datum_from_iter(..)
  2. The new fast pre-allocated method

Each test used 10,000 rows of randomly generated int[] in a temp table, work_mem = '1GB'. I used Instant::now() to time only the microseconds needed to convert and accumulated all those timings

    #[pg_extern]
    fn accum_alloc(input: Vec<i32>) -> i32 {
        let start = std::time::Instant::now();
        let array = input.into_datum().unwrap();  // this uses array_datum_from_iter(..)
        let duration = start.elapsed();
        duration.subsec_micros() as i32
    }
    
    #[pg_extern]
    fn fast_alloc(input: Vec<i32>) -> i32 {
        let start = std::time::Instant::now();
        let array = Array::<i32>::new_from_slice(input.as_slice()).expect("couldn't allocate");
        let duration = start.elapsed();
        duration.subsec_micros() as i32
    }
Row Count int[] Length accum_alloc (total μs) fast_alloc (total μs) Improvement
10,000 10 16,933.00 2,120.00 8.0×
10,000 100 155,352.00 10,228.00 15.2×
10,000 1,000 1,375,613.00 30,443.00 45.2×
10,000 10,000 13,666,835.00 251,825.00 54.3×

It's been a while since I contributed, so I'm a little rusty on matching the teams style/conventions, and as usual lifetime differences between PG allocated and rust allocated still escape me. I'm sure the ergonomics of the new Array functions could be improved. Am I doing anything the wrong way here?

Comment thread pgrx/src/array.rs

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

Some initial comments, mostly style nits. Let's discuss the motivation more.

If memory serves, I believe an array is a varlena allocation that does not have to match the precise size of the array's length, yes? That is, the varlena can be allocated bigger than the array needs. Why not something more like the spare_capacity_mut API that Vec has, so we can construct things without first zeroing them? I believe the zeroing overhead is in many cases inconsequential and often optimized-out anyways, but we can always make our code easier to optimize.

Comment thread pgrx/src/array.rs
let elem_size = std::mem::size_of::<T>();
let nbytes: usize = port::ARR_OVERHEAD_NONULLS(1) + elem_size * len;

unsafe {

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.

In new code, a safe function with internal unsafe should explain why the code is sound.

Comment thread pgrx/src/datum/array.rs Outdated
Comment thread pgrx/src/datum/array.rs Outdated
Comment thread pgrx/src/datum/array.rs Outdated
Comment thread pgrx/src/array.rs
Comment thread pgrx/src/array/port.rs Outdated
Comment thread pgrx/src/array/port.rs Outdated
@mhov

mhov commented Sep 3, 2025

Copy link
Copy Markdown
Contributor Author

@usamoi @workingjubilee instead of all the T: Sized + IntoDatum + UnboxDatum stuff should we just make a opt-in trait like we do for RangeSubType that we specifically implement for i8,i16,i32,i64,f32,f64 ?

@workingjubilee

Copy link
Copy Markdown
Member

@mhov Probably so, I think that would be a better starting point anyways as then it would be clearer what we're asking for.

Comment thread pgrx/src/datum/array.rs Outdated
Co-authored-by: Jubilee <workingjubilee@gmail.com>
Comment thread pgrx/src/datum/array.rs Outdated
@mhov

mhov commented Oct 30, 2025

Copy link
Copy Markdown
Contributor Author

i made as_mut_slice internal, and reworked the Array::<T:ArrayFastAllocSubType> constructors under a single impl<'mcx, T> Array<'mcx, T> where T: ArrayFastAllocSubType { .. } including one that can construct from any Iterator<Item = T:ArrayFastAllocSubType> and one with a FnOnce to allow a limited shot at the allocated Array's mut slice.

Let me know what you think about the ergonomics and soundness of Array::<T>::new_from_iter(..) and Array::<T>::new_with_init_fn(..)

Comment thread pgrx/src/array.rs
/// Slightly faster than new_array_type_with_len(0)
pub fn new_empty_array_type<T>() -> Result<RawArray, ArrayAllocError>
where
T: ArrayFastAllocSubType,

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.

This can just use the Scalar trait bound. Hmm. I should probably move that into pgrx::array I guess, since it will outlast datum::Array.

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

With this new API, is it possible to create an Array<'static, T>?

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

Unfortunately, I believe this PR isn't suitable, because while talking about it with @mhov we discovered it's possible to pick the 'static lifetime for a new Array<'static, T> made this way. That shouldn't work, basically, which means we can't accept this PR even in its current form, sadly. I'm working on fixing up an older branch I had made to be its replacement.

I'm leaving this open because I'm treating it as an issue/feature request, essentially.

@mhov

mhov commented Nov 14, 2025

Copy link
Copy Markdown
Contributor Author

All good, thanks @workingjubilee

workingjubilee added a commit that referenced this pull request Nov 30, 2025
Introduce a new Rust type for what ArrayTypes "really are": FlatArray.
As an unsized type, it will primarily be used as `&FlatArray<'_, T>` or
similar in actual extension code. The long-term plan is for FlatArray to
phase out `pgrx::datum::Array<'_, T>` entirely, as it has several
advantages over its predecessor:

- It uses `Element`, a subset of `BorrowDatum`, to power its simplicity
in iteration, making it more easily verifiable and maintainable.
- It is the unsized type itself, allowing composing it with various
Rusty pointer types like `&`, `&mut`, or `PBox`.
- It is no longer endangered by performance cliffs or hidden allocations
depending on the type of `T`.
- Because of these previous traits, it is possible to soundly and safely
allocate a fresh `FlatArray`, mutate it, and return that one allocation,
instead of allocating an intermediate Vec in Rust memory to mutate!

It also has an associated cost in that it no longer supports any `T`
that requires complex unboxing of items like `datum::Array<'a, String>`.
You must instead use `array.iter().map(closure)` to accomplish that.
This itself summarizes the difference: `datum::Array` is effectively an
iteration protocol, but `array::FlatArray` is an actual data structure.

This addresses the implied feature request in, and thus supercedes,
#2086.
@workingjubilee

Copy link
Copy Markdown
Member

I have landed #2207, thus addressing this.

daamien pushed a commit to daamien/pgrx that referenced this pull request Dec 15, 2025
Introduce a new Rust type for what ArrayTypes "really are": FlatArray.
As an unsized type, it will primarily be used as `&FlatArray<'_, T>` or
similar in actual extension code. The long-term plan is for FlatArray to
phase out `pgrx::datum::Array<'_, T>` entirely, as it has several
advantages over its predecessor:

- It uses `Element`, a subset of `BorrowDatum`, to power its simplicity
in iteration, making it more easily verifiable and maintainable.
- It is the unsized type itself, allowing composing it with various
Rusty pointer types like `&`, `&mut`, or `PBox`.
- It is no longer endangered by performance cliffs or hidden allocations
depending on the type of `T`.
- Because of these previous traits, it is possible to soundly and safely
allocate a fresh `FlatArray`, mutate it, and return that one allocation,
instead of allocating an intermediate Vec in Rust memory to mutate!

It also has an associated cost in that it no longer supports any `T`
that requires complex unboxing of items like `datum::Array<'a, String>`.
You must instead use `array.iter().map(closure)` to accomplish that.
This itself summarizes the difference: `datum::Array` is effectively an
iteration protocol, but `array::FlatArray` is an actual data structure.

This addresses the implied feature request in, and thus supercedes,
pgcentralfoundation#2086.
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.

3 participants