Repository navigation
MAINT: refactor StringDType static_string implementation a bit. - #25890
Conversation
The current code is meant to track when a string in the arena is freed for possible re-use later, but actually zeros the offset when an entry is set to undefined, making this impossible. Furthermore, it does not track when the offset is reused for either a short string or a string on the heap, after which the arena should not longer be an option. This commit fixes both problems, aiming to keep the changes minimally intrusive.
mhvk
left a comment
There was a problem hiding this comment.
See inline comments - I hope you agree this is a bit easier to follow...
| // empty arena, nothing to do | ||
| return 0; | ||
| size_t size = VSTRING_SIZE(str); | ||
| if (size == 0 || |
There was a problem hiding this comment.
I felt code should alert one if it is misused (and would probably have used assert myself)
There was a problem hiding this comment.
Shall I make the assert actually act on the expression? (well, the negated one)
Then, it is not used at all in optimized code, which seems OK.
| _npy_static_string_u *out_u = (_npy_static_string_u *)out; | ||
| unsigned char flags = out_u->direct_buffer.size_and_flags & | ||
| ~NPY_SHORT_STRING_SIZE_MASK; | ||
| memcpy(out_u, &empty_string_u, sizeof(_npy_static_string_u)); |
There was a problem hiding this comment.
This copy makes sense in setting the element to a zero-length short string, but after it, the element has lost the offset in the arena, so the flags now have to indicate that.
| return ret; | ||
| } | ||
|
|
||
| NPY_NO_EXPORT int |
There was a problem hiding this comment.
To match location in .h file.
| const npy_packed_static_string *s2, npy_string_allocator *a2) { | ||
| if (a1 != a2 || is_not_a_vstring(s1) || is_not_a_vstring(s2)) { | ||
| if (a1 != a2 || | ||
| is_short_string(s1) || is_short_string(s2) || |
There was a problem hiding this comment.
I could see the case for reintroducing is_a_vstring here just to negate it. But with use in just a single function, it seemed fine to just write it out.
|
Thanks for taking a look at this! I think I'd like to combine this with the other PR just to make both of them easier to review together (and if I have time and motivation, update the NEP too so it can continue to serve as a reference). Do you mind if I push commits to your PR branch implementing that? I have some other stuff on the backburner so it might take a few days to get to. |
|
Yes, please just push to my PR! And for sure fine to do it together; the reason for #25882 was to separate the bugfix from the refactor, which came about mostly because my first refactor was giving segfaults, yet I knew I had encountered a real bug... |
|
I haven't forgotten about this and am planning to work on it today so it can hopefully get included with the 2.0 RC. That said, I think we could put this in after the RC and it shouldn't have any user-visible consequences (besides bug fixes, of course). |
ngoldbaum
left a comment
There was a problem hiding this comment.
Wow this is great. I have a couple clarifying questions below and I pushed a commit that adds a couple of assert(0) calls and cleans up / adds some more comments. I think this is ready to merge and I'd like to get it in the RC to make backports easier for chuck but maybe @seberg could weigh in too.
| #define NPY_STRING_INITIALIZED 0x40 // 0100 0000 | ||
| #define NPY_STRING_OUTSIDE_ARENA 0x20 // 0010 0000 | ||
| #define NPY_STRING_LONG 0x10 // 0001 0000 | ||
| #define NPY_STRING_FLAG_MASK 0xF0 // 1111 0000 |
There was a problem hiding this comment.
Nice, I like that we leave the last word of the flags byte zeroed. That gives us space to write a fixed-width version of stringdtype that always uses SSO but with a user-specified width.
| // empty arena, nothing to do | ||
| return 0; | ||
| size_t size = VSTRING_SIZE(str); | ||
| if (size == 0 || |
| unsigned char flags = | ||
| out_u->direct_buffer.size_and_flags & ~NPY_SHORT_STRING_SIZE_MASK; | ||
|
|
||
| if (size == 0) { |
There was a problem hiding this comment.
I think I initially added the clearing to make it easier for me to debug this. If we don't clear then fragments of old strings can survive in the arena buffer if we mutate an arena string to a smaller size and I was getting confused by that, it also seemed awkward to check for that case in newsize.
For now let's not clear the buffer and leave fragments of old strings behind since it's faster and simpler.
| c = np.zeros_like(self.a) | ||
| assert_array_equal(self.get_flags(c), 0) | ||
| c[:] = self.a | ||
| assert_array_equal(self.get_flags(c), self.get_flags(self.a)) |
There was a problem hiding this comment.
neat, in the old implementation this would have required heap allocations because we'd be enlarging strings in an array that has already been initialized but now that we have the initialized flag this "just works".
There was a problem hiding this comment.
I was actually somewhat surprised it worked... But I agree it is good that it does.
Also describe general logic on top of file, and liberally add comments that hopefully will clarify the code.
|
@ngoldbaum - I was perhaps unreasonably playing with making |
df4fa96 to
2a26376
Compare
|
OK, I couldn't resist: the last commit is the new bit that allows re-use of the arena after setting to an empty string (the rest is just what was there before, but with your commits squashed with mine). EDIT: I find it quite remarkable how little I had to change to make this work - the design is clearly good!! If you agree the empty-string stuff is OK, then perhaps easiest to merge this, and deal with the shared allocators between copies separately (this is actually a bit bad, because the shared arena effectively means that memory does not get freed properly if you delete one of the copies). p.s. I discovered that copies shared the allocator by looking at the offsets; for the copy, these were behind the strings in the original. |
|
Good catch! We might need a hook in the copy code path to ensure copies get a unique descriptor. |
FWIW, the following would add a test that a copy has a new allocator: (it currently fails). |
|
Let's merge this now despite the issue with copies, just in case the RC does happen this weekend. I'd like to make backports easier for Chuck. |
|
Thanks! On the copies, maybe @seberg has ideas about how to prevent a copy being simply assigned the same dtype as the original (and thus the same allocator for |
Isn't it just that void also needs the dtype finalization function which calls the finalization recursively on all fields? Or did my first glance go in the wrong direction? |
No, this has nothing to do with voids. Marten's doing a hack to even make the void view possible. We probably just need a call to |
|
It ended up being something else but thankfully the fix was in stringdtype itself so no need to update the dtype API or fix a bug in numpy. See #25944. |
@ngoldbaum - this is the refactor I mentioned. It started with wanting to use just 4 flags, and trying to make that a bit more logical. But it also solves a number of subtle bugs. In particular, in quite a few places, a
memcpyof the empty string was done when the idea was to keep the arena information alive -- which of course is not possible if one overwrites the offset. I'll put a few more guiding comments in-line.