Repository navigation
BUG: Fix copy performance regression for structured arrays - #29270
Conversation
|
So CI fails because of this It seems to me that the shortcut I added is triggering here which causes the test to fail. In my mind, in this case we shouldn't be using the shortcut to copy - I would expect |
|
Yeah, but it seems |
|
What do you mean by "pushing the logic lower"? |
Sorry, nvm. you are already as low as it gets, I thought we might be able to include the logic in that follows, but it's effectively at the same place. Signaling a trivially copyable would probably be the most useful thing to do, but it may require doing so in general in casts or maybe as a dtype flag (I am not 100% sure a dtype flag covers everything, but probably; The As a quicker fix that doesn't add a flag, I honestly don't have much of an idea except recursing or just giving up when you find an embedded structured dtype (and keep the assumption that all others without references are fine). (Although, I would be happy to investigate a new flag, since that seems useful in a lot more places to do similar optimizations.) |
|
Ignoring embedded structures sounds good to me, I'll try this out. Agree a flag might be a better solution long-term :) |
|
Thanks, could you see how annoying it is to fold the logic into the branch after wards? Otherwise (If it's annoying, I think I can accept not doing it. I hid the fields in NumPy 2, with the hope that we could have a better way to store this information that extract it from tuples every time again and again anyway...) |
|
so I've played around with it and couldn't quite figure out how to access the "underlying" field type information. basically in the example from the test: I could get the description of the field I could just check that the field dtype is not void, I'd go with that if there is no better way, but it seems to be too broad of a check |
That dtype is a subarray dtype, which should be OK, you could check the base I guess (of course, that could be another subarray dtype. I dunno, my gut feeling is that if you don't take the trivial route here, adding the flag is probably actually the easier solution (and that could be a negative slag on a whim that such dtypes are rare). |
|
I've updated the PR: added a @seberg would you have a chance to take another look? |
seberg
left a comment
There was a problem hiding this comment.
Thanks for looking into this! There are test failures, need to check whether they are just a reasonable change or a real hole in the logic (may need to also improve the tests a bit eventually).
One comment I have is that unless we can figure out on the fly that the flag needs to be set, it would be nice to refactor it to only set it at the main creation function and not in the helper (but that requires changing the code a bit).
As the inline comment says, I would love to set it more judiciously, but I can't think of a great way to do so :/.
(Although I might seriously consider setting it for all new struct dtypes that don't use offsets=. In theory a breaking change, but in practice it seems unlikely to hit anyone really with a release note -- although a kwarg to force the flag either way might be needed.)
(I had thought we might even unset the flag if we create a new array, but that means creating a new dtype. That is all possible, but a new dtype might also lead to funny things. For a newly created structured dtype, that might be a neat approach, but I doubt it is good here.)
| #define NPY_FROM_FIELDS (NPY_NEEDS_INIT | NPY_LIST_PICKLE | \ | ||
| NPY_ITEM_REFCOUNT | NPY_NEEDS_PYAPI) | ||
| NPY_ITEM_REFCOUNT | NPY_NEEDS_PYAPI | \ | ||
| NPY_NOT_TRIVIALLY_COPYABLE) |
There was a problem hiding this comment.
I suspect this is good, not really all that public anyway (the flag won't work/exist on older NumPy versions, but if you cast to a uint8 that flag would just disappear).
| PyDataType_ISTRIVIALLYCOPYABLE(context->descriptors[0]) && | ||
| PyDataType_ISTRIVIALLYCOPYABLE(context->descriptors[1])) { |
There was a problem hiding this comment.
| PyDataType_ISTRIVIALLYCOPYABLE(context->descriptors[0]) && | |
| PyDataType_ISTRIVIALLYCOPYABLE(context->descriptors[1])) { | |
| PyDataType_ISTRIVIALLYCOPYABLE(context->descriptors[1])) { |
It shouldn't matter whether the source is trivially copy-able, since we are only worried about overwriting another field.
| /* Mark as not trivially copyable if layout is not simple (has padding) */ | ||
| if (!is_dtype_struct_simple_unaligned_layout((PyArray_Descr *)new)) { | ||
| new->flags |= NPY_NOT_TRIVIALLY_COPYABLE; | ||
| } |
There was a problem hiding this comment.
I would love to not do this, i.e. a way to have a struct that is flagged as "no holes" even if align=True is used and there is padding (since nobody cares about padding!).
That said after continuing with it, it seems a bit hard. I could see enabling this flag only when offsets= is used (but it could be a breaking change in theory).
(I.e. to allow this for align=True at least which seems rather worthwhile, with offsets= I guess it we would need a new kwarg...)
There was a problem hiding this comment.
I implemented what I think was your suggestion. So we only set the flag when explicit offsets= is provided. And that means that aligned structs can still use fast path copy since alignment padding is safe to overwrite.
| /* Mark as not trivially copyable if layout is not simple (has padding) */ | ||
| if (!is_dtype_struct_simple_unaligned_layout((PyArray_Descr *)new)) { | ||
| new->flags |= NPY_NOT_TRIVIALLY_COPYABLE; | ||
| } |
There was a problem hiding this comment.
I think we can drop this field for pickles because we shouldn't need it on any newly created dtype. (I.e. actually the other way around: we could even always drop the field when pickling I would think?)
There was a problem hiding this comment.
Not 100% sure I did this correctly, I am now dropping the field from pickels, please double check.
| /* Mark as not trivially copyable if layout is not simple (has padding) */ | ||
| if (!is_dtype_struct_simple_unaligned_layout((PyArray_Descr *)view_dtype)) { | ||
| view_dtype->flags |= NPY_NOT_TRIVIALLY_COPYABLE; | ||
| } |
There was a problem hiding this comment.
To me, this one should be the most interesting one. Because here we need to always set it since we probably removed fields.
It would be nice to make that smarter, but I am not even sure if it is too worthwhile/necessary, especially to get started with testing.
|
|
||
| /* Types with fields need the Python C API for field access */ | ||
| char dtypeflags = NPY_NEEDS_PYAPI; | ||
| npy_uint64 dtypeflags = NPY_NEEDS_PYAPI; |
There was a problem hiding this comment.
Fixed what I think was the reason for the test failures: dtypeflags was char but NPY_NOT_TRIVIALLY_COPYABLE (0x100) requires 9 bits. Changed to npy_uint64
|
Hi @seberg, just wanted to check this is still on your radar. I've pushed changes addressing your review comments - let me know if you'd like anything else adjusted. |
|
Thanks, I think this makes sense, but I need to bring it up once at least in the meeting (maybe ping the mailing list just in case). My thoughts are along these lines:
But overall, I am not really worried about breaking people. It is super weird to create a struct and rely on alignment to leave holes that we actually want to access. If you are excited, adding a helper like EDIT: Oooops, sorry, you have the ISTRIVIALLYCOPYABLE already of course... Could/should be used in a few other places still. If you care, you can try the speed for advanced indexing with this dtype for example. |
| return -1; | ||
| } | ||
| *flags = PyArrayMethod_MINIMAL_FLAGS; | ||
| } |
There was a problem hiding this comment.
Ah, one small comment. I think we could lift this out to also kick in for the below path?
(Although, not sure I care: That only kicks in if a subarray field is identical, but another field is not.)
There was a problem hiding this comment.
I looked into this too but I think I prefer keeping the check inside the named-fields branch as it is now. It makes the intent more explicit — you can see that the fast path is specifically for structured dtypes with fields.
|
Regarding the trivially_copyable opt-out kwarg I'm not sure this is necessary in practice. This PR restores the original memcpy behavior for contiguous structured dtypes. So users would have had memcpy semantics (including padding bytes being copied) in the version before that regression was introduced. I wouldn't expect anyone to have started relying on padding byte preservation in the narrow window since then. Happy to implement the opt-out if others feel it's needed though, something like a adding a |
Yeah, except that has been a bit too long that this was changed :). Either way, I agree, this is interesting, but probably only for cases we have covered (i.e. I am actually more interested in setting But, that is a good followup! |
|
@seberg wanted to confirm the current state: you will bring this change up the next internal meeting due to it being a breaking change and get approval that this change can merge? Are there any outstanding comments/changes you would like to be addressed in this PR? There are a couple of optional follow-ups, but nothing blocking this PR from merging, right? |
|
Arg, thanks for the ping. I had forgotten to post what the conclusion was a bit:
That said, I am not sure I want to force that on this PR. One thing that actually would be good is to proof the big speedup by adding an asv benchmark. |
|
Benchmark results: |
|
@seberg when you have a chance can you update the labels on the PR? Right now it has Needs Release Note which I've already added. Do you think this will get into the next point release (2.4.3)? |
No, we don't backport such changes, 2.4.x is bug fixes only. It will be in 2.5 (mid of the year). |
|
(we could backport, but I think there is a chance of regression?!) |
|
Agree with not backporting this change, let's wait for the next release (2.5) |
|
OK, I think it's time to just get this in, thanks @bonpyt. There are probably smaller follow-ups that would be nice, but none or vital enough to block this on. |
This adds a new dtype flag used by structured dtypes to indicate that they _cannot_ trivially copy (`NPY_NOT_TRIVIALLY_COPYABLE`). This way, dtypes that do not set this (and _also_ don't have the references flag set) can be assumed to be trivially copyable, which should allow certain default implementations/fast-path improvements elsewhere as well. The current small caveat is that this flag cannot be unpickled yet, so it isn't pickled (safe but slower on round-trip) and it may also make sense to allow setting it explicitly. Co-authored-by: bonpyt <[email protected]>
Fixes #28194
This adds a new dtype flag used by structured dtypes to indicate that they cannot trivially copy (
NPY_NOT_TRIVIALLY_COPYABLE).This way, dtypes that do not set this (and also don't have the references flag set) can be assumed to be trivially copyable, which should allow certain default implementations/fast-path improvements elsewhere as well.
The current small caveat is that this flag cannot be unpickled yet, so it isn't pickled (safe but slower on round-trip) and it may also make sense to allow setting it explicitly.