array.c: optimize heap sort with hole-style sift-down and Floyd's method

two improvements to Array#sort!'s heap sort:

1. hole-style sift-down: save root value, move larger children up
   one at a time, write saved value once at the end. reduces
   assignments from 3 per level (swap) to 1 per level (move).

2. Floyd's bottom-up heap deletion: during extraction phase, sift
   the hole down to a leaf using only child-child comparisons
   (~1 comparison per level), then sift up to find the correct
   position. this reduces average comparisons from ~2 log n to
   ~log n per extraction, nearly halving the total comparison
   count for the sort.

both changes preserve O(n log n) worst case and O(1) extra space.

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Yukihiro "Matz" Matsumoto
2026-03-20 16:48:30 +09:00
parent e292d7a6c4
commit 02bb943960
+61 -26
View File
@@ -2137,35 +2137,68 @@ sort_cmp(mrb_state *mrb, mrb_value ary, mrb_value a_val, mrb_value b_val, mrb_va
return cmp > 0;
}
/* Hole-style sift-down: save root, move larger children up, write once at end.
Reduces assignments from 3 per level (swap) to 1 per level (move). */
static void
heapify(mrb_state *mrb, mrb_value ary, mrb_value *a, mrb_int index, mrb_int size, mrb_value blk)
{
/* Iterative heapify to avoid stack overflow on memory-constrained devices */
mrb_value val = a[index]; /* save root to hole */
mrb_gc_protect(mrb, val);
while (1) {
mrb_int max = index;
mrb_int left_index = 2 * index + 1;
mrb_int right_index = left_index + 1;
mrb_int child = 2 * index + 1;
if (child >= size) break;
if (left_index < size && sort_cmp(mrb, ary, a[left_index], a[max], blk)) {
max = left_index;
}
if (right_index < size && sort_cmp(mrb, ary, a[right_index], a[max], blk)) {
max = right_index;
/* pick the larger child */
if (child + 1 < size && sort_cmp(mrb, ary, a[child + 1], a[child], blk)) {
child++;
}
/* if hole value >= larger child, done */
if (!sort_cmp(mrb, ary, a[child], val, blk)) break;
if (max == index) {
/* Heap property satisfied, no more swaps needed */
break;
}
/* Swap elements and continue heapifying down the affected subtree */
mrb_value tmp = a[max];
a[max] = a[index];
a[index] = tmp;
/* Continue with the affected child subtree */
index = max;
a[index] = a[child]; /* move child up */
index = child;
}
a[index] = val; /* place saved value */
}
/* Floyd's bottom-up heap deletion: sift the hole down to a leaf without
comparing against the removed root, then sift up from the leaf position.
This reduces comparisons from ~2 log n to ~log n per extraction,
because most elements end up near the bottom of the heap anyway. */
static void
heap_delete_root(mrb_state *mrb, mrb_value ary, mrb_value *a, mrb_int size, mrb_value blk)
{
/* a[0] already holds the value to be re-inserted (set by caller) */
mrb_value last = a[0];
mrb_gc_protect(mrb, last);
/* Phase 1: sift the hole down to a leaf (only child-child comparisons) */
mrb_int hole = 0;
mrb_int child = 1;
while (child + 1 < size) {
/* pick the larger child - 1 comparison per level */
if (sort_cmp(mrb, ary, a[child + 1], a[child], blk)) {
child++;
}
a[hole] = a[child];
hole = child;
child = 2 * hole + 1;
}
/* handle single child at bottom */
if (child < size) {
a[hole] = a[child];
hole = child;
}
/* Phase 2: sift up from hole to find correct position for last */
while (hole > 0) {
mrb_int parent = (hole - 1) / 2;
if (!sort_cmp(mrb, ary, last, a[parent], blk)) break;
a[hole] = a[parent];
hole = parent;
}
a[hole] = last;
}
static void
@@ -2216,15 +2249,17 @@ mrb_ary_sort_bang(mrb_state *mrb, mrb_value ary)
insertion_sort(mrb, ary, a, n, blk);
}
else {
/* Use heap sort for larger arrays */
/* Heap sort with Floyd's bottom-up deletion */
/* Phase 1: build max-heap (standard sift-down, hole style) */
for (mrb_int i = n / 2 - 1; i >= 0; i--) {
heapify(mrb, ary, a, i, n, blk);
}
/* Phase 2: extract max elements using Floyd's method */
for (mrb_int i = n - 1; i > 0; i--) {
mrb_value tmp = a[0];
a[0] = a[i];
a[i] = tmp;
heapify(mrb, ary, a, 0, i, blk);
mrb_value max = a[0];
a[0] = a[i]; /* temporary for GC safety */
a[i] = max; /* max goes to final position */
heap_delete_root(mrb, ary, a, i, blk);
}
}
return ary;