Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: develop
Choose a base branch
from

Conversation

mhov
Copy link
Contributor

@mhov mhov commented Jun 24, 2025

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?

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.

1 participant