gh-135551: Change how sorting picks minimum run length (#135553) · wrongnull/cpython@2fc68e1 · GitHub
Skip to content

Commit 2fc68e1

Browse files
authored
pythongh-135551: Change how sorting picks minimum run length (python#135553)
New scheme from Stefan Pochmann for picking minimum run lengths. By allowing them to change a little from one run to the next, it's possible to arrange for that all merges, at all levels, strongly tend to be as evenly balanced as possible, for randomly ordered data. Meaning the number of initial runs is a power of 2, and all merges involve runs whose lengths differ by no more than 1.
1 parent b38810b commit 2fc68e1

4 files changed

Lines changed: 184 additions & 41 deletions

File tree

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Sorting randomly ordered lists will often run a bit faster, thanks to a new scheme for picking minimum run lengths from Stefan Pochmann, which arranges for the merge tree to be as evenly balanced as is possible.

Objects/listobject.c

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1685,10 +1685,7 @@ sortslice_advance(sortslice *slice, Py_ssize_t n)
16851685
/* Avoid malloc for small temp arrays. */
16861686
#define MERGESTATE_TEMP_SIZE 256
16871687

1688-
/* The largest value of minrun. This must be a power of 2, and >= 1, so that
1689-
* the compute_minrun() algorithm guarantees to return a result no larger than
1690-
* this,
1691-
*/
1688+
/* The largest value of minrun. This must be a power of 2, and >= 1 */
16921689
#define MAX_MINRUN 64
16931690
#if ((MAX_MINRUN) < 1) || ((MAX_MINRUN) & ((MAX_MINRUN) - 1))
16941691
#error "MAX_MINRUN must be a power of 2, and >= 1"
@@ -1749,6 +1746,11 @@ struct s_MergeState {
17491746
* of tuples. It may be set to safe_object_compare, but the idea is that hopefully
17501747
* we can assume more, and use one of the special-case compares. */
17511748
int (*tuple_elem_compare)(PyObject *, PyObject *, MergeState *);
1749+
1750+
/* Varisbles used for minrun computation. The "ideal" minrun length is
1751+
* the infinite precision listlen / 2**e. See listsort.txt.
1752+
*/
1753+
Py_ssize_t mr_current, mr_e, mr_mask;
17521754
};
17531755

17541756
/* binarysort is the best method for sorting small arrays: it does few
@@ -2210,6 +2212,14 @@ merge_init(MergeState *ms, Py_ssize_t list_size, int has_keyfunc,
22102212
ms->min_gallop = MIN_GALLOP;
22112213
ms->listlen = list_size;
22122214
ms->basekeys = lo->keys;
2215+
2216+
/* State for generating minrun values. See listsort.txt. */
2217+
ms->mr_e = 0;
2218+
while (list_size >> ms->mr_e >= MAX_MINRUN) {
2219+
++ms->mr_e;
2220+
}
2221+
ms->mr_mask = (1 << ms->mr_e) - 1;
2222+
ms->mr_current = 0;
22132223
}
22142224

22152225
/* Free all the temp memory owned by the MergeState. This must be called
@@ -2687,27 +2697,15 @@ merge_force_collapse(MergeState *ms)
26872697
return 0;
26882698
}
26892699

2690-
/* Compute a good value for the minimum run length; natural runs shorter
2691-
* than this are boosted artificially via binary insertion.
2692-
*
2693-
* If n < MAX_MINRUN return n (it's too small to bother with fancy stuff).
2694-
* Else if n is an exact power of 2, return MAX_MINRUN / 2.
2695-
* Else return an int k, MAX_MINRUN / 2 <= k <= MAX_MINRUN, such that n/k is
2696-
* close to, but strictly less than, an exact power of 2.
2697-
*
2698-
* See listsort.txt for more info.
2699-
*/
2700-
static Py_ssize_t
2701-
merge_compute_minrun(Py_ssize_t n)
2700+
/* Return the next minrun value to use. See listsort.txt. */
2701+
Py_LOCAL_INLINE(Py_ssize_t)
2702+
minrun_next(MergeState *ms)
27022703
{
2703-
Py_ssize_t r = 0; /* becomes 1 if any 1 bits are shifted off */
2704-
2705-
assert(n >= 0);
2706-
while (n >= MAX_MINRUN) {
2707-
r |= n & 1;
2708-
n >>= 1;
2709-
}
2710-
return n + r;
2704+
ms->mr_current += ms->listlen;
2705+
assert(ms->mr_current >= 0); /* no overflow */
2706+
Py_ssize_t result = ms->mr_current >> ms->mr_e;
2707+
ms->mr_current &= ms->mr_mask;
2708+
return result;
27112709
}
27122710

27132711
/* Here we define custom comparison functions to optimize for the cases one commonly
@@ -3075,7 +3073,6 @@ list_sort_impl(PyListObject *self, PyObject *keyfunc, int reverse)
30753073
/* March over the array once, left to right, finding natural runs,
30763074
* and extending short natural runs to minrun elements.
30773075
*/
3078-
minrun = merge_compute_minrun(nremaining);
30793076
do {
30803077
Py_ssize_t n;
30813078

@@ -3084,6 +3081,7 @@ list_sort_impl(PyListObject *self, PyObject *keyfunc, int reverse)
30843081
if (n < 0)
30853082
goto fail;
30863083
/* If short, extend to min(minrun, nremaining). */
3084+
minrun = minrun_next(&ms);
30873085
if (n < minrun) {
30883086
const Py_ssize_t force = nremaining <= minrun ?
30893087
nremaining : minrun;

Objects/listsort.txt

Lines changed: 159 additions & 16 deletions

0 commit comments

Comments
 (0)