Skip to content
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

Use zero-copy Arrays in --release #1116

Merged
merged 26 commits into from
Apr 20, 2023
Merged

Conversation

workingjubilee
Copy link
Member

@workingjubilee workingjubilee commented Apr 20, 2023

This introduces a pile of "array-walking" code to implement truly "zero-copy" arrays... in release mode. It may make tests notably slower for a bit, because it includes a pile of assertions. Afterwards, a lot of the code, especially the assertion code, can be pared back, simplified, or changed into less high-overhead forms once this has been tested to our satisfaction. Ultimately, I prioritized correctness against the previous behavior of our library rather than validating against Postgres per se (because it's not as clear that our new behavior will correctly model Postgres).

There are no external, user-facing API changes required to take advantage of this. In the future we may wish to consider such, as there are further improvements atop this in terms of correctness and performance to be had.

@workingjubilee
Copy link
Member Author

workingjubilee commented Apr 20, 2023

Also to be clear: the new array-walking code is used unconditionally, it's merely that during tests we lug around ye olde "big slice of datums" to assert against (thus making it not "zero-copy"), because I couldn't think of a different way without publicly exposing functions I would rather not.

Comment on lines +382 to +394
// SAFETY: The constructor for this type instantly panics if any nulls are present!
// Thus as an invariant, this will never have to reckon with the nullbitmap.
let element = unsafe { array.bring_it_back_now(*ptr, *curr, false) };
*curr += 1;
*ptr = unsafe { array.one_hop_this_time(*ptr, array.elem_layout) };
element
Copy link
Member Author

Choose a reason for hiding this comment

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

I almost forgot about this one!

We might as well exploit the invariant that this is now an instantly panicking constructor (and has been for a while, actually). That said, because of the way that the internal NullKind type works (where it embeds, on construction, some knowledge about whether the array can have a null), I honestly expect it is not going to be a big improvement that often.

Copy link
Contributor

@eeeebbbbrrrr eeeebbbbrrrr left a comment

Choose a reason for hiding this comment

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

This is really nice. Really really nice.

ptr.unwrap_or(ptr::null())
/// Rips out the underlying `pg_sys::ArrayType` pointer.
/// Note that Array may have caused Postgres to allocate to unbox the datum,
/// and this can hypothetically cause a memory leak if so.
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't see the hypothetical memory leak here?

Copy link
Contributor

Choose a reason for hiding this comment

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

Is it that Array.raw might be detoasted (ie, allocated) and here we are giving the pointer away?

Is that considered a memory leak? I'm not so sure.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, that's the only thing I can think of. I can delete that remark if it's confusing!

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe this is a little more subtle than we think? If we give the pointer back to the user, it becomes their responsibility to free it (or to let it dangle until Postgres does garbage collection, which is probably the right answer), but there's no way for the user to know if it could safely be freed.

Could we just remove this function entirely? I don't see a burning need to give out an ArrayType pointer, do you?

Copy link
Member Author

Choose a reason for hiding this comment

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

One user uses this to go from Array to RawArray, I believe.

self.curr += 1;
Some(element)
let Self { array, curr, ptr } = self;
let is_null = array.null_slice.get(*curr)?;
Copy link
Contributor

Choose a reason for hiding this comment

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

heh, that ? is subtle. Took me a minute to figure out how this function would ever return None.

Also, this code is duplicated down below at line 454... is there room to consolidate the two iterators? I can't see any difference between them.

Copy link
Member Author

Choose a reason for hiding this comment

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

To the first comment, I used let-else to make all the load-bearing ? more obvious.

Copy link
Member Author

Choose a reason for hiding this comment

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

To the second comment, mostly, I tried to avoid any breaking changes to the API, and currently the API does yield two distinct types of iterators.

Copy link
Contributor

Choose a reason for hiding this comment

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

To the second comment, mostly, I tried to avoid any breaking changes to the API, and currently the API does yield two distinct types of iterators.

That's fine for now but we ought to clean it up at some point.

};

// SAFETY: ptr stops at 1-past-end of the array's varlena
debug_assert!(ptr.wrapping_add(offset) <= self.raw.end_ptr());
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

@eeeebbbbrrrr
Copy link
Contributor

Also to be clear: the new array-walking code is used unconditionally, it's merely that during tests we lug around ye olde "big slice of datums" to assert against (thus making it not "zero-copy"), because I couldn't think of a different way without publicly exposing functions I would rather not.

Yeah, I wonder if we should ditch that entirely?

@eeeebbbbrrrr
Copy link
Contributor

How does this handle Postgres multi-dimensional arrays? I don't suppose it does?

I'm not suggesting we make that part of this work, but I guess we need to make sure this doesn't blow up if given a multidimensional array...

@workingjubilee
Copy link
Member Author

Postgres, like everyone else, does a linearizing transform of the multidimensional space, because ultimately it must, as memory on a "flat memory model" processor is a 1-dimensional space, described only by the number line. So this doesn't make us handle multidimensional arrays, but it does allow us to successfully walk a linear array, and Postgres handles multidimensional arrays as a slight embroidery on the handling of linear arrays.

With all of these pieces, PGX can finally disassemble Array at will
with minimal Drop fussing, as Array is no longer managing its own Drop!

To preserve low-level functions such as `Array::into_array_type`,
ManuallyDrop can inhibit the `Toast<RawArray>` from pfreeing itself,
so use that but document the leakage for users.
These debug asserts validate correctness of our varlena realignment code.
Note that the `Size::CStr` and `Size::Fixed, PassBy::Ref` branches
unfortunately seem to be underexercised at the moment by our tests.
Just factors out the array walking code in a reasonable form,
with slightly less optimizations, but also less duplicate code.
There are larger gains to be had in the future,
but this is a reasonable start on this.
Also in general tighten up everything, less doublechecking lens, et cetera.
This also requires correctly checking indices into null_slice as less-than,
not less-or-equal to the length!
Array's null_slice field safely wraps the nullbitmap.
All of its uses justify its presence, so remove plan to drop it.
let-else is more blatant than a single question mark,
and these are all cases where the ? does a lot of work.
@workingjubilee
Copy link
Member Author

I rebased this on develop.

@workingjubilee
Copy link
Member Author

Postgres, like everyone else, does a linearizing transform of the multidimensional space, because ultimately it must, as memory on a "flat memory model" processor is a 1-dimensional space, described only by the number line.

these are really fancy words that mean "yeah uh a sufficiently large 1D array and an appropriately sized 2D array look the same in memory".

@workingjubilee workingjubilee mentioned this pull request Apr 20, 2023
10 tasks
@workingjubilee
Copy link
Member Author

This final commit is an assertion that is correctness-critical and cannot be removed. I realized it was implicit while rereading the code and describing it to someone who asked me about it. If it proves wrong, we can do the same alignment fixups as for varlenas for fixed-width types (in fact, we can do a single fixup once). I am pushing an assertion instead of simply fixing the code because I don't want to believe that Postgres would use an unaligned typlen like this.

@eeeebbbbrrrr
Copy link
Contributor

This final commit is an assertion that is correctness-critical and cannot be removed. I realized it was implicit while rereading the code and describing it to someone who asked me about it. If it proves wrong, we can do the same alignment fixups as for varlenas for fixed-width types (in fact, we can do a single fixup once). I am pushing an assertion instead of simply fixing the code because I don't want to believe that Postgres would use an unaligned typlen like this.

woah

@workingjubilee workingjubilee merged commit 13c4eb7 into develop Apr 20, 2023
@workingjubilee workingjubilee deleted the joy/zero-copy-arrays branch June 20, 2023 17:53
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.

2 participants