Skip to content

ENH: Descending sorts with built-in dtype implementations - #31345

Merged
seberg merged 52 commits into
numpy:mainfrom
MaanasArora:reverse-sorts
May 13, 2026
Merged

seberg merged 52 commits into
numpy:mainfrom
MaanasArora:reverse-sorts

Conversation

@MaanasArora

Copy link
Copy Markdown
Contributor

This is a draft PR depending on #31278 that enables descending sorts on the Python side and adds support to built-in dtypes (bools, ints, floats, dates). It copies the comparison functions rather than the sorts, but the template wrappers are still a bit verbose. I added some tests for only sorting (not argsorting) but more are needed.

Note that SIMD sorts are not used for descending, nor is radixsort, as these would require more changes to the sort functions themselves (radixsort uses raw memory, for example).

AI Disclosure

Claude wrote the (arg)sort_*_descend functions (not the comparisons), and I looked over very briefly so far.

@MaanasArora
MaanasArora marked this pull request as draft April 27, 2026 23:08
Comment thread numpy/_core/src/common/numpy_tag.h Outdated
static int less_equal(type const& a, type const& b) {
return !less(b, a);
}
static int greater(type const& a, type const& b) {

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.

Arguably we can just create new tags, but doing this seems nicer for consolidation.

@MaanasArora MaanasArora changed the title Reverse sorts ENH: Descending sorts with built-in dtype implementations Apr 27, 2026

@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.

OK, I guess it's worth looking if we can't just clean this up... and in the age of agents, maybe just with the help of an agent...

This whole tag business was always annoying. One small part that would help here, is to push the new sort loop creation into C++ so we only have a make_sort_loops_@name@(...) and everything else can use templating without having to export dozens of functions explicitly.

(And even if this tag business makes sense, I really don't get why it can't use a subclassed template to clean this up. Probably also a C++11 limitation?)

EDIT: I'll see if I can't make a gross simplification in a separate PR quickly... Done in gh-31352, probably more possible, but a minimal start that is probably sufficient for this...

Comment thread numpy/_core/src/common/numpy_tag.h Outdated
}
};

template <typename Tag>

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.

I think we can just use bool reverse now, maybe that wasn't possible with C++11 (where all of this text monstrosity comes from).

@seberg

seberg commented Apr 28, 2026

Copy link
Copy Markdown
Member

One more thing: It seems to me that both highway and x86 sort should have a descending parameter. For highway, I didn't immediately find whether its NaN handling is what we need, but we can probably just try (if not, CI should fail as we'll have some test for it :)).

@MaanasArora

MaanasArora commented Apr 28, 2026 •

Copy link
Copy Markdown
Contributor Author

push the new sort loop creation into C++ so we only have a make_sort_loops_@name@(...)

Push into C++ or do you mean .c.src? But yes, if you mean including this in arraytypes.c.src, we can easily use templating for that I think, with name, tag name, and whether to use SIMD dispatch.

For highway, I didn't immediately find whether its NaN handling is what we need, but we can probably just try (if not, CI should fail as we'll have some test for it :)).

Will do this and above now! We actually already have a basic test for NaN handling for descending in this PR (though only floats I think)!

Edit: I have a draft for quicksort, but let me wait for the other two PRs to merge to rebase before pushing or this will be challenging to rebase :)

@MaanasArora

MaanasArora commented Apr 29, 2026 •

Copy link
Copy Markdown
Contributor Author

I rebased over the other PR, and moved the sort loops registration to C++, it really is much nicer!

I've pushed something broken... the issue is mainly, though, that I'm including .cpp files directly (!) because I'm a bit hesitant to rename/move to .hpp and then just take npy_*sort functions out. It might be hard to review the line-by-line changes otherwise. Not really sure how to move forward here, I guess if we're going to clean this up just moving the files would make sense, maybe after a brief review? Thanks!

Edit: I've moved the template code to header files, and everything works well! So if you like I can push after you have a brief pass at the line-by-line changes?

@MaanasArora

Copy link
Copy Markdown
Contributor Author

I've pushed what I have now - going to clean things up and add back the ArrFuncs.

@MaanasArora

MaanasArora commented Apr 30, 2026 •

Copy link
Copy Markdown
Contributor Author

I did a couple more iterations:

  • Added back sorting arrfuncs during registration
  • Fixed some critical bugs, especially with string and unicode sorting
  • Added a few tests for both sorts and argsorts, with randomization and nan tests
  • Made sure the stubs match
    • including masked arrays but following patterns for stable, raise on descending
  • Made sure linting passed

What is remaining is adding SIMD sorts (though not sure if that should be follow-up, it's not too invasive, but still requires some changes to x86_simd_sort), and cleaning up the Tag::template cmp (I'm having some trouble because it might be repetitive to keep creating a helper).

But I think this should be mostly ready now! Thanks.

Edit: oh, and need a release note, and some explanatory docs maybe.

@MaanasArora
MaanasArora marked this pull request as ready for review April 30, 2026 06:34
@seberg seberg added the 56 - Needs Release Note. Needs an entry in doc/release/upcoming_changes label Apr 30, 2026
@MaanasArora

MaanasArora commented May 1, 2026 •

Copy link
Copy Markdown
Contributor Author

OK, I pushed SIMD sorts for ascending, still trying to check if descending can be included. Also added a release note. Aside from this, mainly just cleanup remaining, hopefully.

Edit: nevermind, some strange bug with the SIMD on some systems... (Seems fixed now.)

@MaanasArora

MaanasArora commented May 3, 2026 •

Copy link
Copy Markdown
Contributor Author

The SIMD bug was for np::half, seems solved now!

I was able to thread descending through SIMD, but as far as I can tell even x86 simd sort doesn't sort nans to the end on descending, so haven't pushed. Closer look, and it seems nans are replaced with positive infinity for both orders.

@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.

Thanks, two more small nits, I'll have another look over the tests, the large moves and things later, but I think this is settling.

(One silly thing: I find the _generic name a bit awkward, because it's exactly the other file that implements the "generic" version that uses the cmp function. Not sure whats best, maybe _templates or even swapping the name -- anyway, just a nit. )

Comment thread numpy/_core/src/common/numpy_tag.h Resolved
@MaanasArora

MaanasArora commented May 4, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks for reviewing! It seems the greater function for complex tag had another bug, which is that we didn't special case NaNs for the real part. We didn't do it for less either, but perhaps got away because nans are always less in c floats. I pushed a fix, if that looks correct, should we mirror it for less or keep it as is?

We do need better tests for complex nans, given three "types" of (heterogenous) nans! Will try to draft some soon.

I find the _generic name a bit awkward, because it's exactly the other file that implements the "generic" version

Ha, I was thinking of generics on types aka templates and somehow thought in the moment that would be clear - I'll probably just swap the names unless you have a preference for another name!

@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.

Thanks, I should go through once more (but I am not sure I'll get to it this week).

But I think this is ready now so if anyone else wants to take a look, please do, because otherwise I may just make a pass put it in (probably early next week).

r += make_sorts_<npy::timedelta_tag>(&PyArray_TimedeltaDType, "timedelta");
r += make_string_sorts_<npy::string_tag>(&PyArray_BytesDType, "string");
r += make_string_sorts_<npy::unicode_tag>(&PyArray_UnicodeDType, "unicode");
r += make_sorts_<npy::half_tag>(&PyArray_HalfDType, "half");

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.

Hmmm, we don't have error handling on the other side now. (Although, it probably doesn't matter much, if this fails some Python error will bubble up and that is all that matters. -- it may be slightly nicer to deal with the errors right away, with a && chain or just a loop, although that may need making some local struct array to loop.)

@MaanasArora MaanasArora May 5, 2026 •

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 right - I made a suggestion in another comment, though yeah, not sure if it's much better given it's not chained!

* Add sorting array methods for the new types.
*/

register_all_sorts();

@MaanasArora MaanasArora May 5, 2026 •

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.

Suggested change
register_all_sorts();
if (register_all_sorts() < 0) {
return -1;
}

Perhaps this suffices, import will crash anyway this way?

@MaanasArora

Copy link
Copy Markdown
Contributor Author

Thanks for reviewing! I swapped the names for quicksort/timsort_generic. I'll try to look at better complex nan tests soon!

@jorenham
jorenham requested a review from seiko2plus May 6, 2026 18:45
@MaanasArora

MaanasArora commented May 7, 2026 •

Copy link
Copy Markdown
Contributor Author

Added various complex tests (maybe too complex :)) and fixed a SIMD dispatch bug.

@charris brought up benchmarks in the community call, so I ran them against the latest commit.

Sort Benchmarks
| Change   | Before [12e16ab3] <main>   | After [ed45342a] <reverse-sorts~1>   |   Ratio | Benchmark (Parameter)                                                         |
|----------|----------------------------|--------------------------------------|---------|-------------------------------------------------------------------------------|
| +        | 8.25±0.05ms                | 76.9±0.7ms                           |    9.32 | bench_function_base.Sort.time_sort('heap', 'float64', ('random',))            |
| +        | 13.2±0.2ms                 | 71.4±0.2ms                           |    5.4  | bench_function_base.Sort.time_sort('heap', 'int64', ('random',))              |
| +        | 4.13±0.08ms                | 21.5±0.07ms                          |    5.21 | bench_function_base.Sort.time_sort('merge', 'int16', ('sorted_block', 100))   |
| +        | 4.73±0.02ms                | 18.8±0.1ms                           |    3.98 | bench_function_base.Sort.time_sort('heap', 'float32', ('sorted_block', 10))   |
| +        | 5.02±0.02ms                | 16.3±0.06ms                          |    3.24 | bench_function_base.Sort.time_sort('heap', 'uint32', ('sorted_block', 10))    |
| +        | 4.32±0.04ms                | 13.3±0.07ms                          |    3.08 | bench_function_base.Sort.time_sort('merge', 'int16', ('sorted_block', 1000))  |
| +        | 4.90±0.01ms                | 13.1±0.08ms                          |    2.68 | bench_function_base.Sort.time_sort('heap', 'float32', ('sorted_block', 100))  |
| +        | 5.24±0.04ms                | 12.5±0.04ms                          |    2.39 | bench_function_base.Sort.time_sort('heap', 'int32', ('sorted_block', 10))     |
| +        | 8.98±0.05ms                | 21.0±0.1ms                           |    2.33 | bench_function_base.Sort.time_sort('heap', 'float64', ('sorted_block', 10))   |
| +        | 6.45±3ms                   | 13.5±0.04ms                          |    2.09 | bench_function_base.Sort.time_sort('merge', 'int16', ('sorted_block', 10))    |
| +        | 4.75±0.2ms                 | 76.0±0.1ms                           |   16    | bench_function_base.Sort.time_sort('heap', 'float32', ('random',))            |
| +        | 4.92±0.05ms                | 69.1±0.2ms                           |   14.06 | bench_function_base.Sort.time_sort('heap', 'uint32', ('random',))             |
| +        | 4.55±0.4ms                 | 63.7±0.08ms                          |   13.99 | bench_function_base.Sort.time_sort('merge', 'int16', ('random',))             |
| +        | 5.10±0.09ms                | 69.5±0.5ms                           |   13.63 | bench_function_base.Sort.time_sort('heap', 'int32', ('random',))              |
| +        | 5.30±0.01ms                | 9.79±0.04ms                          |    1.85 | bench_function_base.Sort.time_sort('heap', 'float32', ('sorted_block', 1000)) |
| +        | 511±20μs                   | 844±2μs                              |    1.65 | bench_function_base.Sort.time_sort('heap', 'float32', ('uniform',))           |
| +        | 8.96±0.09ms                | 14.2±0.06ms                          |    1.59 | bench_function_base.Sort.time_sort('heap', 'float64', ('sorted_block', 100))  |
| +        | 5.32±0.03ms                | 7.67±0.02ms                          |    1.44 | bench_function_base.Sort.time_sort('heap', 'uint32', ('sorted_block', 100))   |
| +        | 5.50±0.06ms                | 7.83±0.07ms                          |    1.42 | bench_function_base.Sort.time_sort('heap', 'int32', ('sorted_block', 100))    |
| +        | 376±7μs                    | 532±40μs                             |    1.42 | bench_function_base.Sort.time_sort('heap', 'uint32', ('uniform',))            |
| +        | 378±30μs                   | 490±10μs                             |    1.29 | bench_function_base.Sort.time_sort('heap', 'int32', ('uniform',))             |
| +        | 9.20±0.1ms                 | 10.9±0.2ms                           |    1.18 | bench_function_base.Sort.time_sort('heap', 'float64', ('sorted_block', 1000)) |
| +        | 691±20μs                   | 786±30μs                             |    1.14 | bench_function_base.Sort.time_sort('heap', 'int64', ('uniform',))             |
| +        | 58.2±0.4ms                 | 63.7±0.3ms                           |    1.1  | bench_function_base.Sort.time_sort('heap', 'int16', ('random',))              |
| +        | 13.9±0.03ms                | 14.7±0.03ms                          |    1.05 | bench_function_base.Sort.time_sort('heap', 'int64', ('sorted_block', 10))     |
| -        | 20.1±2ms                   | 18.8±0.08ms                          |    0.94 | bench_function_base.Sort.time_sort('merge', 'float32', ('sorted_block', 10))  |
| -        | 49.8±9ms                   | 46.6±0.1ms                           |    0.93 | bench_function_base.Sort.time_sort('quick', 'int16', ('sorted_block', 10))    |
| -        | 58.8±4ms                   | 53.8±0.2ms                           |    0.92 | bench_function_base.Sort.time_sort('quick', 'int16', ('sorted_block', 100))   |
| -        | 83.6±10ms                  | 76.0±0.1ms                           |    0.91 | bench_function_base.Sort.time_sort('merge', 'float32', ('random',))           |
| -        | 13.9±2ms                   | 12.5±0.06ms                          |    0.9  | bench_function_base.Sort.time_sort('merge', 'int32', ('sorted_block', 10))    |
| -        | 23.2±0.9ms                 | 20.9±0.06ms                          |    0.9  | bench_function_base.Sort.time_sort('quick', 'int16', ('ordered',))            |
| -        | 85.7±8ms                   | 76.4±0.1ms                           |    0.89 | bench_function_base.Sort.time_sort('merge', 'float64', ('random',))           |
| -        | 17.1±3ms                   | 14.9±0.2ms                           |    0.87 | bench_function_base.Sort.time_sort('merge', 'int64', ('sorted_block', 10))    |
| -        | 8.87±1ms                   | 7.68±0.08ms                          |    0.87 | bench_function_base.Sort.time_sort('merge', 'uint32', ('sorted_block', 100))  |
| -        | 6.60±1ms                   | 5.72±0.05ms                          |    0.87 | bench_function_base.Sort.time_sort('merge', 'uint32', ('sorted_block', 1000)) |
| -        | 57.0±10ms                  | 49.1±0.1ms                           |    0.86 | bench_function_base.Sort.time_sort('quick', 'int16', ('sorted_block', 1000))  |
| -        | 9.43±2ms                   | 7.85±0.04ms                          |    0.83 | bench_function_base.Sort.time_sort('merge', 'int32', ('sorted_block', 100))   |
| -        | 586±60μs                   | 488±9μs                              |    0.83 | bench_function_base.Sort.time_sort('merge', 'uint32', ('uniform',))           |
| -        | 432±20μs                   | 356±20μs                             |    0.82 | bench_function_base.Sort.time_sort('quick', 'int32', ('uniform',))            |
| -        | 935±200μs                  | 701±20μs                             |    0.75 | bench_function_base.Sort.time_sort('merge', 'float32', ('reversed',))         |
| -        | 24.9±0.2ms                 | 18.3±0.2ms                           |    0.74 | bench_function_base.Sort.time_sort('heap', 'float16', ('random',))            |
| -        | 14.1±0.09ms                | 9.55±0.05ms                          |    0.68 | bench_function_base.Sort.time_sort('heap', 'int64', ('sorted_block', 100))    |
| -        | 34.9±10ms                  | 22.2±0.2ms                           |    0.64 | bench_function_base.Sort.time_sort('quick', 'float16', ('sorted_block', 10))  |
| -        | 8.95±3ms                   | 5.42±0.03ms                          |    0.61 | bench_function_base.Sort.time_sort('quick', 'int16', ('uniform',))            |
| -        | 14.4±0.07ms                | 7.36±0.06ms                          |    0.51 | bench_function_base.Sort.time_sort('heap', 'int64', ('sorted_block', 1000))   |
| -        | 22.6±0.05ms                | 11.0±0.3ms                           |    0.49 | bench_function_base.Sort.time_sort('heap', 'float16', ('sorted_block', 100))  |
| -        | 22.8±0.07ms                | 9.62±0.06ms                          |    0.42 | bench_function_base.Sort.time_sort('heap', 'float16', ('sorted_block', 10))   |
| -        | 20.8±0.03ms                | 8.27±0.3ms                           |    0.4  | bench_function_base.Sort.time_sort('heap', 'float16', ('sorted_block', 1000)) |
| -        | 54.9±0.2ms                 | 21.5±0.04ms                          |    0.39 | bench_function_base.Sort.time_sort('heap', 'int16', ('sorted_block', 100))    |
| -        | 7.09±0.2ms                 | 2.10±0.04ms                          |    0.3  | bench_function_base.Sort.time_sort('merge', 'int16', ('ordered',))            |
| -        | 47.9±0.2ms                 | 13.4±0.02ms                          |    0.28 | bench_function_base.Sort.time_sort('heap', 'int16', ('sorted_block', 10))     |
| -        | 50.4±0.1ms                 | 13.3±0.03ms                          |    0.26 | bench_function_base.Sort.time_sort('heap', 'int16', ('sorted_block', 1000))   |
| -        | 4.21±0.09ms                | 862±20μs                             |    0.2  | bench_function_base.Sort.time_sort('heap', 'float32', ('ordered',))           |
| -        | 4.24±0.03ms                | 731±40μs                             |    0.17 | bench_function_base.Sort.time_sort('heap', 'float32', ('reversed',))          |
| -        | 4.55±0.04ms                | 619±10μs                             |    0.14 | bench_function_base.Sort.time_sort('heap', 'uint32', ('reversed',))           |
| -        | 8.46±0.4ms                 | 1.07±0.04ms                          |    0.13 | bench_function_base.Sort.time_sort('heap', 'float64', ('ordered',))           |
| -        | 4.68±0.09ms                | 587±20μs                             |    0.13 | bench_function_base.Sort.time_sort('heap', 'int32', ('reversed',))            |
| -        | 8.40±0.1ms                 | 1.03±0.03ms                          |    0.12 | bench_function_base.Sort.time_sort('heap', 'float64', ('reversed',))          |
| -        | 4.66±0.05ms                | 490±9μs                              |    0.11 | bench_function_base.Sort.time_sort('heap', 'int32', ('ordered',))             |
| -        | 4.57±0.04ms                | 498±20μs                             |    0.11 | bench_function_base.Sort.time_sort('heap', 'uint32', ('ordered',))            |
| -        | 22.4±0.06ms                | 2.11±0.01ms                          |    0.09 | bench_function_base.Sort.time_sort('heap', 'int16', ('ordered',))             |
| -        | 13.2±0.04ms                | 1.07±0.08ms                          |    0.08 | bench_function_base.Sort.time_sort('heap', 'int64', ('reversed',))            |
| -        | 18.0±0.08ms                | 1.27±0ms                             |    0.07 | bench_function_base.Sort.time_sort('heap', 'float16', ('ordered',))           |
| -        | 14.8±0.1ms                 | 1.03±0ms                             |    0.07 | bench_function_base.Sort.time_sort('heap', 'float16', ('uniform',))           |
| -        | 5.60±0.1ms                 | 344±4μs                              |    0.06 | bench_function_base.Sort.time_sort('heap', 'int16', ('uniform',))             |
| -        | 13.3±0.08ms                | 823±8μs                              |    0.06 | bench_function_base.Sort.time_sort('heap', 'int64', ('ordered',))             |
Argsort Benchmarks
| Change   | Before [12e16ab3] <main>   | After [1cdac29d] <reverse-sorts>   |   Ratio | Benchmark (Parameter)                                                             |
|----------|----------------------------|------------------------------------|---------|-----------------------------------------------------------------------------------|
| +        | 11.0±0.4ms                 | 80.7±0.7ms                         |    7.36 | bench_function_base.Sort.time_argsort('merge', 'int16', ('random',))              |
| +        | 8.11±0.2ms                 | 28.8±0.2ms                         |    3.55 | bench_function_base.Sort.time_argsort('merge', 'int16', ('sorted_block', 100))    |
| +        | 32.9±0.3ms                 | 94.9±0.8ms                         |    2.88 | bench_function_base.Sort.time_argsort('heap', 'float32', ('random',))             |
| +        | 34.3±2ms                   | 96.2±2ms                           |    2.81 | bench_function_base.Sort.time_argsort('heap', 'float64', ('random',))             |
| +        | 32.6±0.3ms                 | 87.1±0.6ms                         |    2.67 | bench_function_base.Sort.time_argsort('heap', 'uint32', ('random',))              |
| +        | 42.2±0.8ms                 | 90.6±0.7ms                         |    2.15 | bench_function_base.Sort.time_argsort('heap', 'int64', ('random',))               |
| +        | 8.92±0.2ms                 | 18.5±0.2ms                         |    2.07 | bench_function_base.Sort.time_argsort('merge', 'int16', ('sorted_block', 1000))   |
| +        | 43.8±30ms                  | 89.1±1ms                           |    2.03 | bench_function_base.Sort.time_argsort('heap', 'int32', ('random',))               |
| +        | 9.43±0.2ms                 | 16.6±0.1ms                         |    1.76 | bench_function_base.Sort.time_argsort('merge', 'int16', ('sorted_block', 10))     |
| +        | 16.8±0.2ms                 | 20.7±0.8ms                         |    1.23 | bench_function_base.Sort.time_argsort('merge', 'uint32', ('sorted_block', 10))    |
| +        | 72.8±2ms                   | 80.1±0.4ms                         |    1.1  | bench_function_base.Sort.time_argsort('heap', 'int16', ('random',))               |
| +        | 5.96±0.1ms                 | 6.35±0.07ms                        |    1.07 | bench_function_base.Sort.time_argsort('quick', 'int16', ('uniform',))             |
| +        | 30.7±0.4ms                 | 32.6±0.1ms                         |    1.06 | bench_function_base.Sort.time_argsort('quick', 'int32', ('random',))              |
| -        | 24.4±0.1ms                 | 23.1±0.08ms                        |    0.95 | bench_function_base.Sort.time_argsort('quick', 'float16', ('sorted_block', 10))   |
| -        | 10.3±0.3ms                 | 9.60±0.2ms                         |    0.93 | bench_function_base.Sort.time_argsort('merge', 'float16', ('sorted_block', 1000)) |
| -        | 16.6±0.2ms                 | 15.3±0.2ms                         |    0.92 | bench_function_base.Sort.time_argsort('quick', 'float16', ('uniform',))           |
| -        | 26.1±0.6ms                 | 22.9±0.5ms                         |    0.88 | bench_function_base.Sort.time_argsort('heap', 'float32', ('sorted_block', 10))    |
| -        | 29.3±0.3ms                 | 24.2±0.5ms                         |    0.83 | bench_function_base.Sort.time_argsort('heap', 'float64', ('sorted_block', 10))    |
| -        | 26.7±1ms                   | 20.2±0.4ms                         |    0.76 | bench_function_base.Sort.time_argsort('heap', 'uint32', ('sorted_block', 10))     |
| -        | 1.39±0.03ms                | 967±20μs                           |    0.69 | bench_function_base.Sort.time_argsort('heap', 'float32', ('uniform',))            |
| -        | 1.41±0.07ms                | 944±30μs                           |    0.67 | bench_function_base.Sort.time_argsort('heap', 'float32', ('ordered',))            |
| -        | 1.54±0.2ms                 | 997±30μs                           |    0.65 | bench_function_base.Sort.time_argsort('heap', 'float64', ('ordered',))            |
| -        | 1.06±0.04ms                | 683±40μs                           |    0.65 | bench_function_base.Sort.time_argsort('heap', 'int64', ('ordered',))              |
| -        | 31.5±5ms                   | 20.2±0.2ms                         |    0.64 | bench_function_base.Sort.time_argsort('heap', 'float16', ('random',))             |
| -        | 974±80μs                   | 611±10μs                           |    0.63 | bench_function_base.Sort.time_argsort('heap', 'uint32', ('ordered',))             |
| -        | 1.63±0.09ms                | 992±10μs                           |    0.61 | bench_function_base.Sort.time_argsort('heap', 'float64', ('uniform',))            |
| -        | 29.0±4ms                   | 17.6±0.2ms                         |    0.61 | bench_function_base.Sort.time_argsort('heap', 'int32', ('sorted_block', 10))      |
| -        | 1.08±0.1ms                 | 643±40μs                           |    0.6  | bench_function_base.Sort.time_argsort('heap', 'int32', ('uniform',))              |
| -        | 27.9±0.3ms                 | 16.4±0.5ms                         |    0.59 | bench_function_base.Sort.time_argsort('heap', 'float64', ('sorted_block', 100))   |
| -        | 1.18±0.2ms                 | 684±80μs                           |    0.58 | bench_function_base.Sort.time_argsort('heap', 'int64', ('uniform',))              |
| -        | 29.1±0.2ms                 | 16.3±0.2ms                         |    0.56 | bench_function_base.Sort.time_argsort('heap', 'float32', ('sorted_block', 100))   |
| -        | 36.9±0.7ms                 | 19.3±0.9ms                         |    0.52 | bench_function_base.Sort.time_argsort('heap', 'int64', ('sorted_block', 10))      |
| -        | 27.5±3ms                   | 12.6±0.2ms                         |    0.46 | bench_function_base.Sort.time_argsort('heap', 'float16', ('sorted_block', 100))   |
| -        | 26.1±0.6ms                 | 11.9±0.4ms                         |    0.46 | bench_function_base.Sort.time_argsort('heap', 'float64', ('sorted_block', 1000))  |
| -        | 26.8±0.1ms                 | 11.6±0.1ms                         |    0.43 | bench_function_base.Sort.time_argsort('heap', 'float32', ('sorted_block', 1000))  |
| -        | 25.0±3ms                   | 10.2±0.4ms                         |    0.41 | bench_function_base.Sort.time_argsort('heap', 'float16', ('sorted_block', 1000))  |
| -        | 1.54±0.6ms                 | 630±20μs                           |    0.41 | bench_function_base.Sort.time_argsort('heap', 'uint32', ('uniform',))             |
| -        | 26.5±3ms                   | 10.4±0.06ms                        |    0.39 | bench_function_base.Sort.time_argsort('heap', 'float16', ('sorted_block', 10))    |
| -        | 31.3±2ms                   | 12.4±0.1ms                         |    0.39 | bench_function_base.Sort.time_argsort('heap', 'int32', ('sorted_block', 100))     |
| -        | 75.8±20ms                  | 27.8±0.5ms                         |    0.37 | bench_function_base.Sort.time_argsort('heap', 'int16', ('sorted_block', 100))     |
| -        | 35.9±0.2ms                 | 12.6±0.5ms                         |    0.35 | bench_function_base.Sort.time_argsort('heap', 'int64', ('sorted_block', 100))     |
| -        | 35.9±20ms                  | 11.8±0.4ms                         |    0.33 | bench_function_base.Sort.time_argsort('heap', 'uint32', ('sorted_block', 100))    |
| -        | 2.19±1ms                   | 670±10μs                           |    0.31 | bench_function_base.Sort.time_argsort('heap', 'int32', ('ordered',))              |
| -        | 30.2±2ms                   | 8.67±0.2ms                         |    0.29 | bench_function_base.Sort.time_argsort('heap', 'int32', ('sorted_block', 1000))    |
| -        | 59.1±4ms                   | 16.6±0.09ms                        |    0.28 | bench_function_base.Sort.time_argsort('heap', 'int16', ('sorted_block', 10))      |
| -        | 67.7±5ms                   | 19.1±0.6ms                         |    0.28 | bench_function_base.Sort.time_argsort('heap', 'int16', ('sorted_block', 1000))    |
| -        | 33.7±1ms                   | 8.88±0.1ms                         |    0.26 | bench_function_base.Sort.time_argsort('heap', 'int64', ('sorted_block', 1000))    |
| -        | 18.7±1ms                   | 4.49±0.5ms                         |    0.24 | bench_function_base.Sort.time_argsort('heap', 'float16', ('uniform',))            |
| -        | 15.5±0.3ms                 | 3.79±0.2ms                         |    0.24 | bench_function_base.Sort.time_argsort('merge', 'int16', ('ordered',))             |
| -        | 46.7±20ms                  | 8.34±0.3ms                         |    0.18 | bench_function_base.Sort.time_argsort('heap', 'uint32', ('sorted_block', 1000))   |
| -        | 20.6±1ms                   | 3.02±0.2ms                         |    0.15 | bench_function_base.Sort.time_argsort('heap', 'float16', ('ordered',))            |
| -        | 27.9±1ms                   | 3.47±0.09ms                        |    0.12 | bench_function_base.Sort.time_argsort('heap', 'int16', ('ordered',))              |
| -        | 7.67±3ms                   | 593±10μs                           |    0.08 | bench_function_base.Sort.time_argsort('heap', 'int16', ('uniform',))              |
| -        | 21.2±1ms                   | 986±20μs                           |    0.05 | bench_function_base.Sort.time_argsort('heap', 'float64', ('reversed',))           |
| -        | 22.3±0.6ms                 | 911±20μs                           |    0.04 | bench_function_base.Sort.time_argsort('heap', 'float32', ('reversed',))           |
| -        | 28.2±0.2ms                 | 886±50μs                           |    0.03 | bench_function_base.Sort.time_argsort('heap', 'int64', ('reversed',))             |
| -        | 40.5±20ms                  | 869±30μs                           |    0.02 | bench_function_base.Sort.time_argsort('heap', 'int32', ('reversed',))             |
| -        | 39.0±20ms                  | 832±10μs                           |    0.02 | bench_function_base.Sort.time_argsort('heap', 'uint32', ('reversed',))            |

Basically:

  • "heap" sort behavior is dramatically different, which makes sense because it's not being used anymore (timsort is being used) - not sure if we're actually planning to deprecate it for legacy like this, though?
  • the other regression is for stable sorts (i.e. "merge") on small integers - which is because we don't use radixsort anymore, as @charris suspected, so we should add it back.

Not sure if we should add either in this PR though, the diff may explode. Radixsort needs to be done sometime, as it's a significant regression, though (perhaps moreso for bytes and bools, which are not benchmarked)...

@seberg

seberg commented May 12, 2026

Copy link
Copy Markdown
Member

OK, so besides any unexpected things, the current state/difficulties are:

  1. x86-simd sort descending doesn't work, because it deals with NaNs differently. The fix of that is pretty easy I think, so I am tempted push that as a "performance bug" but follow-up.
  2. descending=True doesn't actually work yet for object and void dtype because they use the old compare slot. Maybe we can also see that as a follow-up:
    • For object it may make sense to just use the new path, but of course unless we add a val != val style check for sorting "unorderable" to the end it doesn't matter.
    • For void, there is nothing to be done quickly, we can only use compare which currently is never implemented to consider NaNs correctly. (I am not sure that is unfathomable in practice, which could be an argument to just refuse descending for it for now...)

Number 1, I would prefer to ignore. Point 2, I am a bit unsure about. Maybe for void we can argue that one should use lexsort and an explicit object loop could be a pretty easy follow-up (even if a bit unnecessarily specialized probably).

@MaanasArora

Copy link
Copy Markdown
Contributor Author

For object it may make sense to just use the new path, but of course unless we add a val != val style check for sorting "unorderable" to the end it doesn't matter.

Is this likely to change any behavior (outside of NumPy)? If it does, probably buggy, but we currently have checks for NULL... I think it's nice to expand using self-comparisons, just trying to think about where to set the boundary for null-checking behavior internally. An edge case could be, e.g., user complex types casted to object, as they don't have a total order and a != a could mean somewhat different things (unless I'm missing something). But probably too much of an edge case! Agreed that, if we can use this check, descending for object should be easy to implement, here or as follow-up.

@MaanasArora

MaanasArora commented May 12, 2026 •

Copy link
Copy Markdown
Contributor Author

Benchmarks for current state:

Sort Benchmarks
| Change   | Before [532a1c5d] <main>   | After [52970f7a] <reverse-sorts>   |   Ratio | Benchmark (Parameter)                                                        |
|----------|----------------------------|------------------------------------|---------|------------------------------------------------------------------------------|
| +        | 12.9±0.08ms                | 16.2±0.1ms                         |    1.26 | bench_function_base.Sort.time_sort('merge', 'uint32', ('sorted_block', 10))  |
| +        | 8.89±0.2ms                 | 10.0±0.09ms                        |    1.13 | bench_function_base.Sort.time_sort('merge', 'int64', ('sorted_block', 100))  |
| +        | 20.4±0.04ms                | 22.9±0.3ms                         |    1.12 | bench_function_base.Sort.time_sort('heap', 'int16', ('ordered',))            |
| +        | 7.00±0.1ms                 | 7.79±0.04ms                        |    1.11 | bench_function_base.Sort.time_sort('merge', 'int64', ('sorted_block', 1000)) |
| +        | 20.6±0.2ms                 | 22.6±0.07ms                        |    1.1  | bench_function_base.Sort.time_sort('quick', 'int16', ('ordered',))           |
| +        | 5.38±0.02ms                | 5.86±0.1ms                         |    1.09 | bench_function_base.Sort.time_sort('heap', 'int16', ('uniform',))            |
| +        | 44.6±0.05ms                | 48.1±0.07ms                        |    1.08 | bench_function_base.Sort.time_sort('heap', 'int16', ('sorted_block', 10))    |
| +        | 51.2±0.09ms                | 55.4±0.2ms                         |    1.08 | bench_function_base.Sort.time_sort('heap', 'int16', ('sorted_block', 100))   |
| +        | 4.71±0.06ms                | 5.11±0.2ms                         |    1.08 | bench_function_base.Sort.time_sort('heap', 'uint32', ('ordered',))           |
| +        | 13.9±0.3ms                 | 15.0±0.3ms                         |    1.08 | bench_function_base.Sort.time_sort('merge', 'int64', ('sorted_block', 10))   |
| +        | 51.4±0.07ms                | 55.4±0.06ms                        |    1.08 | bench_function_base.Sort.time_sort('quick', 'int16', ('sorted_block', 100))  |
| +        | 54.7±0.4ms                 | 58.8±0.6ms                         |    1.07 | bench_function_base.Sort.time_sort('heap', 'int16', ('random',))             |
| +        | 47.1±0.1ms                 | 50.6±0.2ms                         |    1.07 | bench_function_base.Sort.time_sort('heap', 'int16', ('sorted_block', 1000))  |
| +        | 44.9±0.03ms                | 48.0±0.1ms                         |    1.07 | bench_function_base.Sort.time_sort('quick', 'int16', ('sorted_block', 10))   |
| +        | 47.1±0.08ms                | 50.4±0.1ms                         |    1.07 | bench_function_base.Sort.time_sort('quick', 'int16', ('sorted_block', 1000)) |
| +        | 54.9±0.4ms                 | 58.1±0.6ms                         |    1.06 | bench_function_base.Sort.time_sort('quick', 'int16', ('random',))            |
| +        | 5.38±0.1ms                 | 5.72±0.1ms                         |    1.06 | bench_function_base.Sort.time_sort('quick', 'int16', ('uniform',))           |
| -        | 20.4±0.1ms                 | 19.4±0.07ms                        |    0.95 | bench_function_base.Sort.time_sort('merge', 'float32', ('sorted_block', 10)) |
| -        | 9.80±0.05ms                | 9.20±0.04ms                        |    0.94 | bench_function_base.Sort.time_sort('merge', 'float16', ('sorted_block', 10)) |
| -        | 10.7±0.2ms                 | 9.82±0.2ms                         |    0.92 | bench_function_base.Sort.time_sort('heap', 'float64', ('sorted_block', 100)) |
| -        | 23.7±0.1ms                 | 21.0±0.3ms                         |    0.89 | bench_function_base.Sort.time_sort('merge', 'float64', ('sorted_block', 10)) |
| -        | 1.23±0.2ms                 | 998±30μs                           |    0.81 | bench_function_base.Sort.time_sort('heap', 'float64', ('uniform',))          |
ArgSort Benchmarks
| Change   | Before [532a1c5d] <main>   | After [52970f7a] <reverse-sorts>   |   Ratio | Benchmark (Parameter)                                                             |
|----------|----------------------------|------------------------------------|---------|-----------------------------------------------------------------------------------|
| +        | 765±0μs                    | 4.76±0ms                           |    6.23 | bench_function_base.Sort.time_argsort('merge', 'uint32', ('ordered',))            |
| +        | 746±0μs                    | 2.88±0ms                           |    3.86 | bench_function_base.Sort.time_argsort('merge', 'uint32', ('uniform',))            |
| +        | 1.77±0ms                   | 6.64±0ms                           |    3.74 | bench_function_base.Sort.time_argsort('heap', 'float32', ('uniform',))            |
| +        | 774±0μs                    | 2.62±0ms                           |    3.38 | bench_function_base.Sort.time_argsort('merge', 'int32', ('ordered',))             |
| +        | 775±0μs                    | 2.57±0ms                           |    3.32 | bench_function_base.Sort.time_argsort('merge', 'int32', ('uniform',))             |
| +        | 1.57±0ms                   | 5.04±0ms                           |    3.21 | bench_function_base.Sort.time_argsort('merge', 'float16', ('uniform',))           |
| +        | 799±0μs                    | 2.53±0ms                           |    3.17 | bench_function_base.Sort.time_argsort('merge', 'int16', ('uniform',))             |
| +        | 868±0μs                    | 2.64±0ms                           |    3.04 | bench_function_base.Sort.time_argsort('merge', 'int64', ('ordered',))             |
| +        | 953±0μs                    | 2.85±0ms                           |    2.99 | bench_function_base.Sort.time_argsort('merge', 'int64', ('uniform',))             |
| +        | 1.33±0ms                   | 3.91±0ms                           |    2.93 | bench_function_base.Sort.time_argsort('merge', 'float64', ('reversed',))          |
| +        | 1.17±0ms                   | 3.16±0ms                           |    2.71 | bench_function_base.Sort.time_argsort('quick', 'int32', ('ordered',))             |
| +        | 1.39±0ms                   | 3.67±0ms                           |    2.65 | bench_function_base.Sort.time_argsort('heap', 'int64', ('ordered',))              |
| +        | 1.12±0ms                   | 2.94±0ms                           |    2.64 | bench_function_base.Sort.time_argsort('heap', 'int32', ('uniform',))              |
| +        | 1.18±0ms                   | 3.02±0ms                           |    2.56 | bench_function_base.Sort.time_argsort('merge', 'float32', ('ordered',))           |
| +        | 1.22±0ms                   | 3.09±0ms                           |    2.53 | bench_function_base.Sort.time_argsort('merge', 'float64', ('uniform',))           |
| +        | 1.29±0ms                   | 3.23±0ms                           |    2.51 | bench_function_base.Sort.time_argsort('quick', 'int64', ('uniform',))             |
| +        | 1.16±0ms                   | 2.88±0ms                           |    2.48 | bench_function_base.Sort.time_argsort('merge', 'float32', ('uniform',))           |
| +        | 1.40±0ms                   | 3.46±0ms                           |    2.47 | bench_function_base.Sort.time_argsort('merge', 'int32', ('reversed',))            |
| +        | 1.17±0ms                   | 2.78±0ms                           |    2.38 | bench_function_base.Sort.time_argsort('merge', 'float64', ('ordered',))           |
| +        | 1.19±0ms                   | 2.83±0ms                           |    2.37 | bench_function_base.Sort.time_argsort('heap', 'int64', ('uniform',))              |
| +        | 1.30±0ms                   | 3.06±0ms                           |    2.35 | bench_function_base.Sort.time_argsort('merge', 'int64', ('reversed',))            |
| +        | 1.71±0ms                   | 3.98±0ms                           |    2.32 | bench_function_base.Sort.time_argsort('merge', 'float32', ('reversed',))          |
| +        | 1.19±0ms                   | 2.77±0ms                           |    2.32 | bench_function_base.Sort.time_argsort('quick', 'uint32', ('ordered',))            |
| +        | 1.84±0ms                   | 4.08±0ms                           |    2.21 | bench_function_base.Sort.time_argsort('quick', 'float64', ('ordered',))           |
| +        | 1.20±0ms                   | 2.58±0ms                           |    2.15 | bench_function_base.Sort.time_argsort('quick', 'int32', ('uniform',))             |
| +        | 1.23±0ms                   | 2.66±0ms                           |    2.15 | bench_function_base.Sort.time_argsort('quick', 'int64', ('ordered',))             |
| +        | 1.28±0ms                   | 2.68±0ms                           |    2.09 | bench_function_base.Sort.time_argsort('heap', 'uint32', ('ordered',))             |
| +        | 1.79±0ms                   | 3.68±0ms                           |    2.06 | bench_function_base.Sort.time_argsort('heap', 'float32', ('ordered',))            |
| +        | 1.59±0ms                   | 3.17±0ms                           |    1.99 | bench_function_base.Sort.time_argsort('merge', 'uint32', ('reversed',))           |
| +        | 1.68±0ms                   | 3.35±0ms                           |    1.99 | bench_function_base.Sort.time_argsort('quick', 'float32', ('uniform',))           |
| +        | 1.75±0ms                   | 3.43±0ms                           |    1.96 | bench_function_base.Sort.time_argsort('merge', 'float16', ('ordered',))           |
| +        | 1.79±0ms                   | 3.37±0ms                           |    1.89 | bench_function_base.Sort.time_argsort('heap', 'float64', ('ordered',))            |
| +        | 1.56±0ms                   | 2.93±0ms                           |    1.88 | bench_function_base.Sort.time_argsort('heap', 'uint32', ('uniform',))             |
| +        | 1.91±0ms                   | 3.51±0ms                           |    1.84 | bench_function_base.Sort.time_argsort('quick', 'float64', ('uniform',))           |
| +        | 1.55±0ms                   | 2.80±0ms                           |    1.8  | bench_function_base.Sort.time_argsort('heap', 'int32', ('ordered',))              |
| +        | 1.71±0ms                   | 3.04±0ms                           |    1.78 | bench_function_base.Sort.time_argsort('quick', 'float32', ('ordered',))           |
| +        | 1.92±0ms                   | 3.23±0ms                           |    1.68 | bench_function_base.Sort.time_argsort('heap', 'float64', ('uniform',))            |
| +        | 9.27±0ms                   | 14.2±0ms                           |    1.54 | bench_function_base.Sort.time_argsort('merge', 'int16', ('sorted_block', 100))    |
| +        | 1.83±0ms                   | 2.66±0ms                           |    1.45 | bench_function_base.Sort.time_argsort('quick', 'uint32', ('uniform',))            |
| +        | 8.61±0ms                   | 10.8±0ms                           |    1.25 | bench_function_base.Sort.time_argsort('heap', 'int16', ('uniform',))              |
| +        | 9.27±0ms                   | 11.2±0ms                           |    1.21 | bench_function_base.Sort.time_argsort('merge', 'uint32', ('sorted_block', 1000))  |
| +        | 36.3±0ms                   | 43.8±0ms                           |    1.21 | bench_function_base.Sort.time_argsort('quick', 'float64', ('random',))            |
| +        | 10.4±0ms                   | 12.1±0ms                           |    1.17 | bench_function_base.Sort.time_argsort('merge', 'int16', ('sorted_block', 1000))   |
| +        | 19.5±0ms                   | 22.9±0ms                           |    1.17 | bench_function_base.Sort.time_argsort('merge', 'uint32', ('sorted_block', 10))    |
| +        | 23.4±0ms                   | 26.4±0ms                           |    1.13 | bench_function_base.Sort.time_argsort('heap', 'float64', ('reversed',))           |
| +        | 9.67±0ms                   | 10.9±0ms                           |    1.13 | bench_function_base.Sort.time_argsort('merge', 'int32', ('sorted_block', 1000))   |
| +        | 13.7±0ms                   | 15.2±0ms                           |    1.11 | bench_function_base.Sort.time_argsort('merge', 'int32', ('sorted_block', 100))    |
| +        | 10.1±0ms                   | 11.2±0ms                           |    1.11 | bench_function_base.Sort.time_argsort('merge', 'int64', ('sorted_block', 1000))   |
| +        | 26.7±0ms                   | 29.3±0ms                           |    1.1  | bench_function_base.Sort.time_argsort('heap', 'float64', ('sorted_block', 1000))  |
| +        | 17.5±0ms                   | 19.0±0ms                           |    1.09 | bench_function_base.Sort.time_argsort('quick', 'float16', ('uniform',))           |
| +        | 36.5±0ms                   | 39.7±0ms                           |    1.09 | bench_function_base.Sort.time_argsort('quick', 'int64', ('sorted_block', 100))    |
| +        | 33.6±0ms                   | 36.7±0ms                           |    1.09 | bench_function_base.Sort.time_argsort('quick', 'uint32', ('random',))             |
| +        | 12.2±0ms                   | 13.2±0ms                           |    1.08 | bench_function_base.Sort.time_argsort('merge', 'float16', ('sorted_block', 100))  |
| +        | 22.3±0ms                   | 24.1±0ms                           |    1.08 | bench_function_base.Sort.time_argsort('quick', 'uint32', ('reversed',))           |
| +        | 29.8±0ms                   | 32.0±0ms                           |    1.07 | bench_function_base.Sort.time_argsort('heap', 'uint32', ('sorted_block', 100))    |
| +        | 13.7±0ms                   | 14.6±0ms                           |    1.07 | bench_function_base.Sort.time_argsort('merge', 'float32', ('sorted_block', 1000)) |
| +        | 25.8±0ms                   | 27.6±0ms                           |    1.07 | bench_function_base.Sort.time_argsort('quick', 'float16', ('sorted_block', 100))  |
| +        | 17.4±0ms                   | 18.4±0ms                           |    1.06 | bench_function_base.Sort.time_argsort('merge', 'int16', ('ordered',))             |
| +        | 37.4±0ms                   | 39.3±0ms                           |    1.05 | bench_function_base.Sort.time_argsort('heap', 'int64', ('sorted_block', 10))      |
| -        | 25.3±0ms                   | 24.1±0ms                           |    0.95 | bench_function_base.Sort.time_argsort('heap', 'float32', ('reversed',))           |
| -        | 34.2±0ms                   | 32.5±0ms                           |    0.95 | bench_function_base.Sort.time_argsort('heap', 'int32', ('random',))               |
| -        | 24.6±0ms                   | 23.3±0ms                           |    0.95 | bench_function_base.Sort.time_argsort('heap', 'int32', ('reversed',))             |
| -        | 15.0±0ms                   | 14.2±0ms                           |    0.95 | bench_function_base.Sort.time_argsort('merge', 'float64', ('sorted_block', 1000)) |
| -        | 98.4±0ms                   | 93.6±0ms                           |    0.95 | bench_function_base.Sort.time_argsort('merge', 'int64', ('random',))              |
| -        | 25.9±0ms                   | 24.3±0ms                           |    0.94 | bench_function_base.Sort.time_argsort('heap', 'float16', ('sorted_block', 10))    |
| -        | 14.9±0ms                   | 14.0±0ms                           |    0.94 | bench_function_base.Sort.time_argsort('merge', 'uint32', ('sorted_block', 100))   |
| -        | 27.4±0ms                   | 25.8±0ms                           |    0.94 | bench_function_base.Sort.time_argsort('quick', 'uint32', ('sorted_block', 10))    |
| -        | 32.3±0ms                   | 30.2±0ms                           |    0.93 | bench_function_base.Sort.time_argsort('quick', 'int64', ('reversed',))            |
| -        | 36.0±0ms                   | 32.8±0ms                           |    0.91 | bench_function_base.Sort.time_argsort('heap', 'uint32', ('random',))              |
| -        | 36.4±0ms                   | 32.9±0ms                           |    0.9  | bench_function_base.Sort.time_argsort('heap', 'float64', ('random',))             |
| -        | 45.4±0ms                   | 41.0±0ms                           |    0.9  | bench_function_base.Sort.time_argsort('heap', 'int64', ('random',))               |
| -        | 22.5±0ms                   | 19.3±0ms                           |    0.86 | bench_function_base.Sort.time_argsort('merge', 'int32', ('sorted_block', 10))     |
| -        | 28.2±0ms                   | 22.5±0ms                           |    0.8  | bench_function_base.Sort.time_argsort('heap', 'uint32', ('reversed',))            |
| -        | 31.1±0ms                   | 23.4±0ms                           |    0.75 | bench_function_base.Sort.time_argsort('merge', 'float32', ('sorted_block', 10))   |

EDIT(seberg): On my machine for argsort, I get that these all are faster... mergsort is stable-sort which is actually timesort here. But the timesort code didn't change and isn't vectorized :/. So this seems a lot like the compiler doing different choices for random reasons...

Details
| Change   | Before [0adb7ef2] <main>   | After [52970f7a] <reverse-sorts>   |   Ratio | Benchmark (Parameter)                                                            |
|----------|----------------------------|------------------------------------|---------|----------------------------------------------------------------------------------|
| -        | 8.43±0.1ms                 | 7.82±0.2ms                         |    0.93 | bench_function_base.Sort.time_argsort('merge', 'uint32', ('sorted_block', 1000)) |
| -        | 19.2±0.1ms                 | 17.5±0.08ms                        |    0.91 | bench_function_base.Sort.time_argsort('merge', 'float32', ('sorted_block', 10))  |
| -        | 2.40±0.1ms                 | 2.14±0.05ms                        |    0.89 | bench_function_base.Sort.time_argsort('heap', 'float32', ('ordered',))           |
| -        | 2.37±0.1ms                 | 2.12±0.05ms                        |    0.89 | bench_function_base.Sort.time_argsort('heap', 'float32', ('uniform',))           |
| -        | 2.40±0.04ms                | 2.13±0.06ms                        |    0.89 | bench_function_base.Sort.time_argsort('quick', 'float32', ('uniform',))          |
| -        | 2.31±0.06ms                | 2.06±0.07ms                        |    0.89 | bench_function_base.Sort.time_argsort('quick', 'uint32', ('ordered',))           |
| -        | 1.53±0.05ms                | 1.32±0.04ms                        |    0.86 | bench_function_base.Sort.time_argsort('merge', 'float16', ('ordered',))          |
| -        | 919±40μs                   | 779±30μs                           |    0.85 | bench_function_base.Sort.time_argsort('merge', 'int16', ('uniform',))            |
| -        | 2.51±0.1ms                 | 2.14±0.1ms                         |    0.85 | bench_function_base.Sort.time_argsort('quick', 'float32', ('ordered',))          |
| -        | 1.06±0.02ms                | 873±20μs                           |    0.83 | bench_function_base.Sort.time_argsort('merge', 'float64', ('ordered',))          |
| -        | 1.29±0.07ms                | 1.03±0.04ms                        |    0.8  | bench_function_base.Sort.time_argsort('merge', 'float16', ('uniform',))          |
| -        | 2.39±0.01ms                | 1.89±0.01ms                        |    0.79 | bench_function_base.Sort.time_argsort('heap', 'float64', ('uniform',))           |
| -        | 1.17±0.07ms                | 923±30μs                           |    0.79 | bench_function_base.Sort.time_argsort('merge', 'float32', ('reversed',))         |
| -        | 2.40±0.01ms                | 1.88±0ms                           |    0.79 | bench_function_base.Sort.time_argsort('quick', 'float64', ('uniform',))          |
| -        | 1.18±0.01ms                | 916±2μs                            |    0.78 | bench_function_base.Sort.time_argsort('merge', 'float64', ('reversed',))         |
| -        | 2.30±0.05ms                | 1.80±0.07ms                        |    0.78 | bench_function_base.Sort.time_argsort('quick', 'int32', ('ordered',))            |
| -        | 2.30±0.1ms                 | 1.80±0.1ms                         |    0.78 | bench_function_base.Sort.time_argsort('quick', 'int32', ('uniform',))            |
| -        | 2.34±0.09ms                | 1.80±0.1ms                         |    0.77 | bench_function_base.Sort.time_argsort('heap', 'int32', ('ordered',))             |
| -        | 2.32±0.05ms                | 1.80±0.1ms                         |    0.77 | bench_function_base.Sort.time_argsort('heap', 'int32', ('uniform',))             |
| -        | 960±30μs                   | 712±20μs                           |    0.74 | bench_function_base.Sort.time_argsort('merge', 'int32', ('reversed',))           |
| -        | 987±9μs                    | 730±6μs                            |    0.74 | bench_function_base.Sort.time_argsort('merge', 'int64', ('reversed',))           |
| -        | 853±10μs                   | 593±6μs                            |    0.7  | bench_function_base.Sort.time_argsort('merge', 'int64', ('ordered',))            |
| -        | 852±6μs                    | 595±5μs                            |    0.7  | bench_function_base.Sort.time_argsort('merge', 'int64', ('uniform',))            |
| -        | 841±30μs                   | 577±40μs                           |    0.69 | bench_function_base.Sort.time_argsort('merge', 'uint32', ('uniform',))           |
| -        | 852±50μs                   | 573±40μs                           |    0.67 | bench_function_base.Sort.time_argsort('merge', 'uint32', ('ordered',))           |

SOME BENCHMARKS HAVE CHANGED SIGNIFICANTLY.
PERFORMANCE INCREASED.

@seberg

seberg commented May 12, 2026 •

Copy link
Copy Markdown
Member

Since this has been sitting for so long and without any other reviews (and I threatened merging previously), I think I'll put this in tomorrow (Manaas will do a Benchmark round to confirm that existing sorts have no regressions).

EDIT: OK, seems there is an argsort issue, we'll fix that one of course :).

Follow-up comments/reviews are definitely still welcome even afterwards!
I think code wise it turned out quite well (cleaning up a lot of things because it pushes the templating boundary into C++ rather than duplicating it in both .c.src and C++).

As mentioned above there are still two main issues at large:

  • descending=False is slower because x86-dims sort parts are not taken for it right now. (highway sorts are used as it uses the same NaN order convention)
    • Raghuveer has started looking into that, it isn't difficult as such to fix (the work is in x86-simd sort, here it is trivial).
  • object and void don't support it, and I think at least object should. (As mentioned above, lexsort needs fixing, but that is a follow-up.)

If the above choices are controversial, we could disable descending=True in python for 2.5, unless there are implementation concerns. Especially object dtype we can just implement, the question is we deal with NaN nicer or not. (object dtype isn't the only follow-up that makes sense here, lexsort and partitioning does as well and is also something we want to do soon!)


and a != a could mean somewhat different things

Yeah, that is true, but it always seems like the best guess that isn't hard-coding something for numeric types via math.isnan() or so, which doesn't work...

@charris

charris commented May 13, 2026

Copy link
Copy Markdown
Member

Benchmarks for current state:

I'm confused by the labels, heapsort is only internally by introsort (nee quicksort) and should not appear in the benchmarks. Likewise mergesort is now timsort. These should now be labeled as 'default' and 'stable'. Radix sort is also 'stable' and should be used for small integers. A comment in the code outlining those choices would be useful going forward, basically columns, something like: "dtype | default/stable | descending (yes, no) | algorithm". That's a lot of entries, but will say what numpy has chosen. Note that stable algorithms can also be the default, radix sort, for instance. The benchmarks only need dtype, default/stable, ascending/descending. Descending will be new and missing from the previous versions.

@MaanasArora

Copy link
Copy Markdown
Contributor Author

I'm confused by the labels

Yes, the benchmarks are in the exact same state as they were before this PR, i.e. based on the 'legacy' kind parameter, so it makes total sense to update them (in follow up)!

And yes, there is not really a way to specify the algorithm beyond stable and descending, so that is what we need strictly for benchmarking in the future. But if we do lose benchmarking for the kind parameter, in that PR it might also be good to add tests or so ensuring the algorithms are mapped correctly (just noting how we just caught the heapsort mapping bug with these)?

@seberg

seberg commented May 13, 2026

Copy link
Copy Markdown
Member

OK, I don't want to go all the way and look at assembly right now. I followed np::Half and made the helper classes use constexpr (including the new cmp helper; I think this makes a lot of sense and follows precedence).

(On the mac, I had a 20% slowdown on some argsorts, with this it's gone and a general gain if anything -- but likely just random. On my AMD monster machine this PR was already before generally an improvement 🤷 -- although it is now more improved, maybe mainly for float16)

There may be more here, e.g. ensuring a high optimization level for these files which may just make sense, but for now I'll assume this is fine, I think with this the change is more random than anything!

@MaanasArora I'll put this in once tests pass, but if you could try the benchmark ones more that would be interesting.

And for anyone else: Please feel free to ask for follow-ups on anything specifically to this PR!

@MaanasArora

Copy link
Copy Markdown
Contributor Author

Thanks for the improvements @seberg! I ran the benchmarks again, and they seem good, random mostly though argsort does seem to be a bit faster. Results below:

Sort Benchmarks
| Change   | Before [532a1c5d] <main>   | After [92945ce5] <reverse-sorts>   |   Ratio | Benchmark (Parameter)                                                          |
|----------|----------------------------|------------------------------------|---------|--------------------------------------------------------------------------------|
| +        | 12.8±0.06ms                | 16.2±0.06ms                        |    1.26 | bench_function_base.Sort.time_sort('merge', 'uint32', ('sorted_block', 10))    |
| +        | 20.2±0.06ms                | 22.4±0.08ms                        |    1.11 | bench_function_base.Sort.time_sort('heap', 'int16', ('ordered',))              |
| +        | 13.7±0.1ms                 | 15.1±0.2ms                         |    1.1  | bench_function_base.Sort.time_sort('merge', 'int64', ('sorted_block', 10))     |
| +        | 20.2±0.05ms                | 22.3±0.07ms                        |    1.1  | bench_function_base.Sort.time_sort('quick', 'int16', ('ordered',))             |
| +        | 5.20±0.01ms                | 5.65±0.09ms                        |    1.09 | bench_function_base.Sort.time_sort('heap', 'int16', ('uniform',))              |
| +        | 6.86±0.08ms                | 7.50±0.04ms                        |    1.09 | bench_function_base.Sort.time_sort('merge', 'int64', ('sorted_block', 1000))   |
| +        | 5.23±0.02ms                | 5.69±0.09ms                        |    1.09 | bench_function_base.Sort.time_sort('quick', 'int16', ('uniform',))             |
| +        | 44.6±0.06ms                | 48.0±0.1ms                         |    1.08 | bench_function_base.Sort.time_sort('heap', 'int16', ('sorted_block', 10))      |
| +        | 46.9±0.2ms                 | 50.5±0.1ms                         |    1.08 | bench_function_base.Sort.time_sort('heap', 'int16', ('sorted_block', 1000))    |
| +        | 8.97±0.2ms                 | 9.69±0.2ms                         |    1.08 | bench_function_base.Sort.time_sort('merge', 'int64', ('sorted_block', 100))    |
| +        | 54.6±0.3ms                 | 58.4±0.4ms                         |    1.07 | bench_function_base.Sort.time_sort('heap', 'int16', ('random',))               |
| +        | 51.2±0.08ms                | 54.8±0.1ms                         |    1.07 | bench_function_base.Sort.time_sort('heap', 'int16', ('sorted_block', 100))     |
| +        | 44.7±0.5ms                 | 48.0±0.08ms                        |    1.07 | bench_function_base.Sort.time_sort('quick', 'int16', ('sorted_block', 10))     |
| +        | 51.3±0.09ms                | 54.9±0.1ms                         |    1.07 | bench_function_base.Sort.time_sort('quick', 'int16', ('sorted_block', 100))    |
| +        | 47.0±0.06ms                | 50.4±0.3ms                         |    1.07 | bench_function_base.Sort.time_sort('quick', 'int16', ('sorted_block', 1000))   |
| +        | 54.5±0.4ms                 | 57.8±0.3ms                         |    1.06 | bench_function_base.Sort.time_sort('quick', 'int16', ('random',))              |
| -        | 9.62±0.06ms                | 9.12±0.06ms                        |    0.95 | bench_function_base.Sort.time_sort('merge', 'float16', ('sorted_block', 10))   |
| -        | 8.11±0.07ms                | 7.71±0.06ms                        |    0.95 | bench_function_base.Sort.time_sort('merge', 'uint32', ('sorted_block', 100))   |
| -        | 6.01±0.04ms                | 5.68±0.03ms                        |    0.95 | bench_function_base.Sort.time_sort('merge', 'uint32', ('sorted_block', 1000))  |
| -        | 20.2±0.07ms                | 19.0±0.07ms                        |    0.94 | bench_function_base.Sort.time_sort('merge', 'float32', ('sorted_block', 10))   |
| -        | 10.6±0.2ms                 | 9.79±0.04ms                        |    0.93 | bench_function_base.Sort.time_sort('merge', 'float32', ('sorted_block', 1000)) |
| -        | 627±30μs                   | 586±2μs                            |    0.93 | bench_function_base.Sort.time_sort('merge', 'int32', ('reversed',))            |
| -        | 395±30μs                   | 356±3μs                            |    0.9  | bench_function_base.Sort.time_sort('merge', 'int16', ('uniform',))             |
| -        | 23.8±0.4ms                 | 21.1±0.3ms                         |    0.89 | bench_function_base.Sort.time_sort('merge', 'float64', ('sorted_block', 10))   |
| -        | 15.1±0.09ms                | 13.2±1ms                           |    0.87 | bench_function_base.Sort.time_sort('quick', 'float16', ('uniform',))           |
| -        | 806±40μs                   | 695±20μs                           |    0.86 | bench_function_base.Sort.time_sort('heap', 'int64', ('uniform',))              |
| -        | 1.17±0.07ms                | 991±30μs                           |    0.85 | bench_function_base.Sort.time_sort('quick', 'float64', ('uniform',))           |
ArgSort Benchmarks
| Change   | Before [532a1c5d] <main>   | After [92945ce5] <reverse-sorts>   |   Ratio | Benchmark (Parameter)                                                           |
|----------|----------------------------|------------------------------------|---------|---------------------------------------------------------------------------------|
| +        | 582±10μs                   | 634±20μs                           |    1.09 | bench_function_base.Sort.time_argsort('merge', 'int16', ('uniform',))           |
| -        | 30.3±2ms                   | 28.4±0.4ms                         |    0.94 | bench_function_base.Sort.time_argsort('heap', 'int32', ('random',))             |
| -        | 35.6±0.4ms                 | 33.3±0.4ms                         |    0.94 | bench_function_base.Sort.time_argsort('quick', 'int64', ('sorted_block', 1000)) |
| -        | 6.27±0.07ms                | 5.86±0.1ms                         |    0.93 | bench_function_base.Sort.time_argsort('quick', 'int16', ('uniform',))           |
| -        | 19.5±1ms                   | 17.7±0.1ms                         |    0.91 | bench_function_base.Sort.time_argsort('merge', 'int64', ('sorted_block', 10))   |
| -        | 12.5±0.5ms                 | 11.2±0.1ms                         |    0.9  | bench_function_base.Sort.time_argsort('merge', 'int64', ('sorted_block', 100))  |
| -        | 11.1±0.4ms                 | 9.62±0.05ms                        |    0.87 | bench_function_base.Sort.time_argsort('merge', 'int16', ('random',))            |
| -        | 9.28±0.4ms                 | 8.08±0.09ms                        |    0.87 | bench_function_base.Sort.time_argsort('merge', 'int64', ('sorted_block', 1000)) |
| -        | 19.9±0.5ms                 | 15.9±0.3ms                         |    0.8  | bench_function_base.Sort.time_argsort('merge', 'int32', ('sorted_block', 10))   |

Users can now pass the `descending=True` keyword argument to ``numpy.sort`` and ``numpy.argsort``
to sort and argsort arrays in descending order. NaN values, if present, are sorted to the end
of the array in both ascending and descending sorts. This feature is available for all built-in
dtypes except `void`, `object`, and `generic`. Note that SIMD optimizations for sorting are

@MaanasArora MaanasArora May 13, 2026 •

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.

Now that we have Highway, perhaps should say "on some machines" or even "Note that some hardware based performance optimizations..."? @seberg

Edit: perhaps just "some" - "Note that some SIMD optimizations are..."

Perhaps doesn't matter though...

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... I am also hoping we'll fix this before 2.5 anyway (and fix up this note). But feel free to do a follow-up PR maybe?

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 yeah, true! Let's address in follow up, thanks!

@seberg
seberg merged commit 06ef89f into numpy:main May 13, 2026
87 checks passed
@pllim

pllim commented May 18, 2026

Copy link
Copy Markdown
Contributor

Hello. I think this broke astropy (astropy/astropy#19760). What numpy version will this go into?

@seberg

seberg commented May 18, 2026

Copy link
Copy Markdown
Member

Ah, seems we missed that sorting does the arr.sort() method style fallback. For that purpose, we should sanitize it away (i.e. not pass it if it is the default False).

@charris

charris commented May 18, 2026

Copy link
Copy Markdown
Member

What numpy version will this go into?

NumPy 2.5.x.

@pllim

pllim commented May 18, 2026

Copy link
Copy Markdown
Contributor

@jorisvandenbossche

Copy link
Copy Markdown
Contributor

Something else that seems to have changed is that np.argsort(obj) used to call the obj.argsort() if it had that method even if obj is not a numpy array, and now not anymore?

(pandas CI is failing for np.argsort(pd_series) now returning a numpy array where before this returned a pandas Series, and I assume this is because of calling the Series.argsort() method before, but haven't yet investigated in detail)

@MaanasArora

Copy link
Copy Markdown
Contributor Author

@jorisvandenbossche Sorry about that, thanks for reporting! It could possibly be because of this same descending=False bug too, as it seems that if the passed args and kwargs don't work with the obj.argsort (i.e., in this case obj.argsort does not have a kwarg descending), then we fall back to using NumPy's argsort. Maybe checking out #31458 and seeing if that fixes things would help - I can try doing that too!

@jorisvandenbossche

Copy link
Copy Markdown
Contributor

Ah yes of course. Checking the _wrapfunc in numpy, that will indeed raise an error when trying to call Series.argsort() if passing descending. In that case #31458 should indeed fix it

eendebakpt added a commit to eendebakpt/numpy that referenced this pull request Sep 17, 2026
The x86 SIMD paths were gated behind `!reverse` since numpygh-31345, because
x86-simd-sort placed NaNs at the start of a descending result while NumPy
keeps them at the end (see `npy::cmp<Tag, reverse>`).  The vendored library
gained a separate `nans_last` switch in numpy/x86-simd-sort#235, which came
in with the submodule bump in numpygh-31947, so `descending` can now be forwarded
directly: `nans_last` defaults to true and gives exactly NumPy's ordering.

Removes the dispatch guards in quicksort.hpp/selection.hpp and the
`assert(!reverse)` in the three dispatch translation units, and threads
`reverse` through QSelect/ArgQSelect so partition and argpartition are
covered as well.

Measured on AVX2 over 1M elements, descending now runs at the speed of
ascending: ~4-9x faster than before for `sort`/`partition` and ~2-3x for
`argsort`/`argpartition`.

Closes part of numpygh-31423.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
eendebakpt added a commit to eendebakpt/numpy that referenced this pull request Sep 18, 2026
The x86 SIMD paths were gated behind `!reverse` since numpygh-31345, because
x86-simd-sort placed NaNs at the start of a descending result while NumPy
keeps them at the end (see `npy::cmp<Tag, reverse>`).  The vendored library
gained a separate `nans_last` switch in numpy/x86-simd-sort#235, which came
in with the submodule bump in numpygh-31947, so `descending` can now be forwarded
directly: `nans_last` defaults to true and gives exactly NumPy's ordering.

Removes the dispatch guards in quicksort.hpp/selection.hpp and the
`assert(!reverse)` in the three dispatch translation units, and threads
`reverse` through QSelect/ArgQSelect so partition and argpartition are
covered as well.

Measured on AVX2 over 1M elements, descending now runs at the speed of
ascending: ~4-9x faster than before for `sort`/`partition` and ~2-3x for
`argsort`/`argpartition`.

Closes part of numpygh-31423.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
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.

6 participants