Bug report
Bug description:
On randomly ordered data, Python's sort ends up artificially forcing all initial runs to length minrun, whose value is a function of len(list), picked in a cheap way to avoid the very worst cases of imbalance in the final merge.
Stefan Pochmann has invented a much better way, which may vary minrun a little from one run to the next. Under his scheme, at all levels of the merge tree:
- The number of runs is a power of 2.
- At most two different values of run lengths are in play.
- The lengths of run pairs to be merged differ by no more than 1.
So, in all cases, in all ways it's as close to perfectly balanced as possible.
For randomly ordered inputs, this eliminates all cases of "bad imbalance", cuts the average number of compares a little, and cuts the amount of pointer copying due to unbalanced merges.
Following is a Python prototype verifying the claims. Variables starting with "mr_" will become part of the MergeState struct, and the loop body in the generator will become a brief inlined function.
from itertools import accumulate try: from itertools import batched except ImportError: from itertools import islice def batched(xs, k): it = iter(xs) while chunk := tuple(islice(it, k)): yield chunk MAX_MINRUN = 64 def gen_minruns(n): # mr_int = minrun's integral part # mr_frac = minrun's fractional part with mr_e bits and # mask mr_mask mr_int = n mr_e = 0 while mr_int >= MAX_MINRUN: mr_int >>= 1 mr_e += 1 mr_mask = (1 << mr_e) - 1 mr_frac = n & mr_mask mr_current_frac = 0 while True: mr_current_frac += mr_frac assert mr_current_frac >> mr_e <= 1 yield mr_int + (mr_current_frac >> mr_e) mr_current_frac &= mr_mask def chew(n, show=False): if n < 1: return sizes = [] tot = 0 for size in gen_minruns(n): sizes.append(size) tot += size if tot >= n: break assert tot == n print(n, len(sizes)) small, large = 32, 64 while len(sizes) > 1: assert not len(sizes) & 1 assert len(sizes).bit_count() == 1 # i.e., power of 2 assert sum(sizes) == n assert min(sizes) >= min(n, small) assert max(sizes) <= large d = set(sizes) assert len(d) <= 2 if len(d) == 2: lo, hi = sorted(d) assert lo + 1 == hi mr = n / len(sizes) for i, s in enumerate(accumulate(sizes, initial=0)): assert int(mr * i) == s newsizes = [] for a, b in batched(sizes, 2): assert abs(a - b) <= 1 newsizes.append(a + b) sizes = newsizes smsll = large large *= 2 assert sizes[0] == n for n in range(2_000_001): chew(n)
CPython versions tested on:
3.14, CPython main branch
Operating systems tested on:
Windows