ENH: Add `sort_compare` dtype slot to register new-style sorts by MaanasArora · Pull Request #30415 · numpy/numpy · GitHub
Skip to content

ENH: Add sort_compare dtype slot to register new-style sorts#30415

Open
MaanasArora wants to merge 6 commits into
numpy:mainfrom
MaanasArora:enh/sort-compare
Open

ENH: Add sort_compare dtype slot to register new-style sorts#30415
MaanasArora wants to merge 6 commits into
numpy:mainfrom
MaanasArora:enh/sort-compare

Conversation

@MaanasArora

@MaanasArora MaanasArora commented Dec 11, 2025

Copy link
Copy Markdown
Contributor

Follows #29737 and #30328 (and supersedes #29987) to implement a sort_compare slot that can be used to register sorting methods using a comparison function. This functionality also existed in the ArrFuncs slots, but now we implement the new-style sorting methods, and the array method context is passed to the comparison function rather than the array itself.

I've tested this with mpfdtype on the user dtypes repo (though having a bit of an issue with installation... the slot wasn't visible there, so had to redefine just the macro). Unfortunately there don't seem to be dtypes we can write tests for within numpy itself, though I did a small experiment with the scaled float dtype. ping @seberg @mhvk @ngoldbaum if you're interested - thanks!

@ngoldbaum ngoldbaum left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@mhvk and @charris I think had strong opinions about prior versions of this. I'd appreciate it if they could weigh in on the approach.

Overall I think this is a nice cleanup and since StringDType wanted it, I wouldn't be surprised if future DTypes do as well.

Comment thread doc/source/reference/c-api/array.rst Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

maybe add a mention for why you'd want to do this - e.g. if comparisons have some big cost that can be amortized over the whole array

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I've added a note - a bit on the detailed side, hopefully that works, thanks!

@MaanasArora

Copy link
Copy Markdown
Contributor Author

Thanks for looking @ngoldbaum!

Overall I think this is a nice cleanup and since StringDType wanted it, I wouldn't be surprised if future DTypes do as well.

Agreed, the full loop implementation would probably be overkill in many cases. I think this would be necessary to expose the sorts on the user-dtypes too since they don't define full sorts now, so a good way to test the new infrastructure!

@MaanasArora

Copy link
Copy Markdown
Contributor Author

Added release note and support for GIL flag. @seberg Friendly ping if you want to look at this when you get the chance!

@mhvk mhvk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Apologies for the delay in looking at this - it really slipped too far....

This looks mostly great, some small comments in-line.

A bigger question is about the name: should it just be compare? I.e., can it not be used also for dtype comparison more generally, replacing NPY_DT_PyArray_ArrFuncs_compare? I cannot really think what the difference would be... (but if we go that route, we can adjust comparison in a follow-up PR). Let me ping @seberg on this bit...

Relatedly, I think not if we want to get rid of ArrFuncs eventually, but let me ask just in case: would it make sense to by default use NPY_DT_PyArray_ArrFuncs_compare if it is available?

Comment thread numpy/_core/src/multiarray/dtypemeta.c Outdated
.nout = 1,
.dtypes = sort_dtypes,
.slots = sort_slots,
.flags = NPY_METH_NO_FLOATINGPOINT_ERRORS,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Given it being set here, is it still necessary to also do it in npy_default_get_(arg)sort_loop?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ah thanks, good catch! It's not - I'll remove it from there.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It's correct in the get_loop, here it is rather irrelevant (it's a default, but for the default get_loop)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not sure I understand sorry - don't they have the same effect? But I can remove it from here if it's a design / reuse thing.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You can remove it here, it doesn't matter really. The value here is what get_loop uses if you don't provide a custom get_loop. But you do, so it is effectively not used.

Comment thread numpy/_core/src/multiarray/dtypemeta.c Outdated
.flags = NPY_METH_NO_FLOATINGPOINT_ERRORS,
};
if (dtype->flags & NPY_NEEDS_PYAPI) {
sort_spec.flags |= NPY_METH_REQUIRES_PYAPI;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same question here.

Also, in the other places, it uses PyDataType_REFCHK - any reason for the difference?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same here, it's not!

I think PyDataType_REFCHK is used for descriptors and not the dtypemeta (at least type wise).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yeah, NPY_NEEDS_PYAPI flag makes more sense. REFCHK doesn't matter, so should be changed in the first one.

@seberg

seberg commented Mar 17, 2026

Copy link
Copy Markdown
Member

A bigger question is about the name: should it just be compare?

Maybe it's best to keep it this way? The context is based on the sorting, I think it is fine. We could still add a mechanism to return a mechanism for both, but I am tempted to say it's fine to split it. In the end, comparison needs both a superset (<= rather than just <) and a subset (no worries about NaN order)...

I guess this is partly saying that defining the comparisons ufuncs is just easy enough, while sorting is a bit tedious to implement.

@seberg seberg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This looks good, sorry should have pushed this earlier. I am OK with seeing this is a bit distinct to comparisons.

Mainly, because the alternative to me would seem to be having operands for all normal comparisons + variations for NaN order for less and greater.
But... that adds a complexity that I am not sure is all that helpful (because as mentioned adding the ufunc family is pretty straight forward).


Would it be easy to use this for the SFloat (without losing the other test coverage completely since I think string ufuncs use that also?)

Comment thread numpy/_core/src/common/npy_sort.c Outdated
PyArrayMethod_SortParameters *parameters = (PyArrayMethod_SortParameters *)context->parameters;
*flags |= NPY_METH_NO_FLOATINGPOINT_ERRORS;

if (PyDataType_REFCHK(context->descriptors[0])) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

As mentioned below, should use the NPY_REQUIRES_API flag here. Otherwise should be OK, if someone wants to relax this, they'll just have to implement the whole thing...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This (and removing from registration) is done, thanks! (I'll push sfloat changes later)

Comment thread numpy/_core/src/multiarray/dtypemeta.c Outdated
.nout = 1,
.dtypes = sort_dtypes,
.slots = sort_slots,
.flags = NPY_METH_NO_FLOATINGPOINT_ERRORS,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You can remove it here, it doesn't matter really. The value here is what get_loop uses if you don't provide a custom get_loop. But you do, so it is effectively not used.

} PyArrayMethod_SortParameters;

typedef int (PyArrayDTypeMeta_SortCompare)(const void *a, const void *b,
PyArrayMethod_Context *context);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Might even be better this way, but I am slightly partial towards putting the context first, just because it is the way in ufunc loops.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Unfortunately I cast directly to CompareFunc, so seems hard to do?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What if we make a wrapper for the old-style CompareFunc? For the numpy dtypes we could (in a follow-up PR perhaps...) then write explicit loops, in order to keep the same performance.

@MaanasArora

MaanasArora commented Mar 17, 2026

Copy link
Copy Markdown
Contributor Author

No worries at all, thanks for reviewing @mhvk @seberg!

A bigger question is about the name: should it just be compare?

Yes, as Sebastian said, if we use this for comparison, I think we lose some functionality - also, keeping this tied to sorting makes it easier to separate concerns if a dtype has some sort-specific quirk (like some optimization that can be made for sort but not compare due to the superset/subset distinction). If we do add a general compare, it can probably be used to fill this by default, but that's for the future!

One note though, this should also probably be used for binsearch / searchsorted (if it is revamped), as that is similar enough.

Relatedly, I think not if we want to get rid of ArrFuncs eventually, but let me ask just in case: would it make sense to by default use NPY_DT_PyArray_ArrFuncs_compare if it is available?

The context is sort-specific - though it can probably be filled in if we wrap! Aside from that hurting deprecation (I think?) one concern with that might be blurring legacy code i.e. the existing dtypes would suddenly define these array methods (which might be nice, but I didn't go that way yet - though it probably needs to be done one way or the other anyway!)

@MaanasArora

MaanasArora commented Mar 17, 2026

Copy link
Copy Markdown
Contributor Author

Sorry, missed the top-level comment - thanks for reviewing.

Would it be easy to use this for the SFloat (without losing the other test coverage completely since I think string ufuncs use that also?)

Yes I think it's quite easy, in fact IIRC I used it for testing - going to push that!

@mhvk

mhvk commented Mar 17, 2026

Copy link
Copy Markdown
Contributor

Now saw your reply to my bigger comment. I guess I can see that comparison might be different for sort that generally, but really it seems hard to see how that would be the case where one only compares individual elements, not a whole set of them. And for a whole set, one probably just wants to register one's own sort loop.

Overall, it seems fine for comparison to remain as simple as possible and be used wherever (as it is for the old-style function, I think...).

@mhvk

mhvk commented Mar 17, 2026

Copy link
Copy Markdown
Contributor

Just trying to keep playing devil's advocate a bit: the NaN case is indeed one where regular comparisons may different from what is needed for sorting, but wouldn't that be covered by having a context passed in?

@seberg

seberg commented Mar 17, 2026

Copy link
Copy Markdown
Member

but really it seems hard to see how that would be the case where one only compares individual elements, not a whole set of them. And for a whole set, one probably just wants to register one's own sort loop.

Well, they are slightly different if you look ahead:

  1. Normal comparisons: <, >, <=, >=, ==, !=. (I.e. enumerated by Python richcmp also)
  2. Sort comparisons: < + NaN considered largest value, > + NaN considered smallest value
    • Right now, we also have the opposite NaN ordering, although the sort-loop could take care of that as well.
    • We also don't need the stable information here, I think.

I am not opposed at all to trying to merge. The big thing is that we definitely need to change the current sorting implementations (which seems fine) and either keep the old one, or add a mini-wrapper (and live with the slight performance hit).

Changing+wrapping/duplicating the generic sort loop is fine. I think the main thing is that context really needs to be different. If just int compare(PyArray_Descr *descr, void *left, void *right, CMP_ENUM cmp) function, or even a get_compare() to allow things like locking/unlocking (for string dtype) and passing in NPY_AUXDATA/specialization.

@MaanasArora

Copy link
Copy Markdown
Contributor Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants