]> git.ipfire.org Git - thirdparty/git.git/commitdiff
prio-queue: use cascade-down for faster extract-min
authorKristofer Karlsson <krka@spotify.com>
Mon, 1 Jun 2026 08:17:35 +0000 (08:17 +0000)
committerJunio C Hamano <gitster@pobox.com>
Mon, 1 Jun 2026 10:11:29 +0000 (19:11 +0900)
Add sift_up_rebalance(), an alternative to sift_down_root() that
halves the number of comparisons per extract-min.

The standard extract places the last array element at the root and
sifts it down.  At each level this requires two comparisons (left
vs right child, then element vs winner) and a swap.

sift_up_rebalance() instead promotes the smaller child into the
root slot at each level — one comparison and one copy — until the
vacancy reaches a leaf.  The last array element is placed at the
vacancy and sifted up to restore heap order.  In practice the
sift-up rarely moves more than a level or two because the last
array element tends to be large.

Work per extract drops from 2d comparisons + d swaps to
d comparisons + d copies + a short sift-up.

prio_queue_get() now calls sift_up_rebalance() instead of placing
the last element at root and calling sift_down_root().

sift_down_root() and prio_queue_replace() are left unchanged.

Synthetic benchmark (10 rounds of 10M put+get cycles, CPU-pinned,
same compiler and Makefile flags):

Ascending keys (git's typical pattern — parents have lower
priority than children):

  queue width  baseline  patched  speedup
           10     4.39s    3.91s    1.12x
          100     9.10s    6.61s    1.38x
        1,000    11.84s    9.25s    1.28x
       10,000    17.50s   13.92s    1.26x
      100,000    23.97s   20.19s    1.19x

Descending keys (worst case — last element always sinks to leaf):

  queue width  baseline  patched  speedup
           10     4.94s    4.95s    1.00x
          100     9.75s    9.42s    1.03x
        1,000    15.01s   15.29s    0.98x
       10,000    24.79s   23.88s    1.04x
      100,000    29.69s   28.24s    1.05x

Random keys:

  queue width  baseline  patched  speedup
           10     5.05s    4.99s    1.01x
          100     9.90s    9.50s    1.04x
        1,000    15.35s   14.77s    1.04x
       10,000    25.35s   24.21s    1.05x
      100,000    65.71s   63.38s    1.04x

No regressions in any scenario.

End-to-end benchmark on the linux kernel repo (1.4M commits,
range v5.0..v6.0, 311K commits, 20 interleaved runs, 1 warmup):

  Command                      baseline  patched  speedup
  rev-list --count v5.0..v6.0    484ms     474ms    1.02x

The improvement scales with DAG width: wider DAGs produce larger
priority queues, amplifying the per-level savings.  In small or
narrow repositories the queues stay shallow and the sift-down
cost is already negligible.

Signed-off-by: Kristofer Karlsson <krka@spotify.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
prio-queue.c

index 9748528ce6ecd6f3d1e707884bc62693fbda0bf4..66d445b07800c05773d487dd427d622011502fab 100644 (file)
@@ -76,6 +76,29 @@ static void sift_down_root(struct prio_queue *queue)
        }
 }
 
+static void sift_up_rebalance(struct prio_queue *queue)
+{
+       size_t ix, child;
+
+       /* Cascade: promote smaller child at each level. */
+       for (ix = 0; (child = ix * 2 + 1) < queue->nr; ix = child) {
+               if (child + 1 < queue->nr &&
+                   compare(queue, child, child + 1) >= 0)
+                       child++;
+               queue->array[ix] = queue->array[child];
+       }
+
+       /* Place the last element at the vacancy and sift up. */
+       queue->array[ix] = queue->array[queue->nr];
+       while (ix) {
+               size_t parent = (ix - 1) / 2;
+               if (compare(queue, parent, ix) <= 0)
+                       break;
+               swap(queue, parent, ix);
+               ix = parent;
+       }
+}
+
 void *prio_queue_get(struct prio_queue *queue)
 {
        void *result;
@@ -89,8 +112,7 @@ void *prio_queue_get(struct prio_queue *queue)
        if (!--queue->nr)
                return result;
 
-       queue->array[0] = queue->array[queue->nr];
-       sift_down_root(queue);
+       sift_up_rebalance(queue);
        return result;
 }