-
-
Notifications
You must be signed in to change notification settings - Fork 259
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
Conversation
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. |
// 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 |
There was a problem hiding this comment.
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.
There was a problem hiding this 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. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
pgrx/src/datum/array.rs
Outdated
self.curr += 1; | ||
Some(element) | ||
let Self { array, curr, ptr } = self; | ||
let is_null = array.null_slice.get(*curr)?; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
Yeah, I wonder if we should ditch that entirely? |
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... |
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.
5df479a
to
cc219bf
Compare
I rebased this on develop. |
cc219bf
to
4ba1369
Compare
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". |
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 |
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.