prio-queue: fold lazy_queue into prio_queue for automatic get+put fusion

Defer the actual removal in prio_queue_get() until the next operation. If that next operation is a prio_queue_put(), the removal and insertion are fused into a single replace — writing the new element at the root and sifting it down — which avoids a full remove-rebalance-insert cycle. This matches the dominant usage pattern in git's commit traversal: get a commit, then put its parents. The first parent insertion after each get is now a replace operation automatically. This generalizes the lazy_queue pattern from builtin/describe.c (introduced in 08bb69d70f) into prio_queue itself. Three callers independently implemented the same get+put fusion: - builtin/describe.c had a full lazy_queue wrapper - commit.c:pop_most_recent_commit() used peek+replace - builtin/show-branch.c:join_revs() used peek+replace All three now collapse to plain _get() and _put(), with the data structure handling the fusion internally. This simplifies callers and means every prio_queue user gets the optimization for free without needing to implement it manually. Remove prio_queue_replace() since no external callers remain. Benchmarked on a 1.8M-commit monorepo (30 interleaved runs, paired t-test, Xeon @ 2.20GHz): Code paths that previously did eager get+put (new optimization): Command base patched change p merge-base --all A A~1000 3828ms 3725ms -2.69% 0.0001 rev-list --count A~1000..A 3055ms 2986ms -2.27% 0.0601 log --oneline A~1000..A 3408ms 3350ms -1.71% 0.0482 Code paths that already had manual get+put fusion (expect neutral — the optimization moves into prio_queue but the number of heap operations stays the same): Command base patched change p show-branch A A~1000 9156ms 9127ms -0.32% 0.3470 describe (4751 revs, 81K repo) 1983ms 1963ms -1.02% <0.001 No regressions in any scenario. Suggested-by: René Scharfe <l.s.r@web.de> Signed-off-by: Kristofer Karlsson <krka@spotify.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Kristofer Karlsson committed Jun 8, 2026 at 19:10 UTC 9f75e7a150edfe931391047bf84c697b4b15c4c4
6 files changed +78 -132
builtin/describe.c
+16 -59
@@ -251,61 +251,19 @@ static int compare_pt(const void *a_, const void *b_)
251 return 0;
252 }
253
254 -struct lazy_queue {
255 - struct prio_queue queue;
256 - bool get_pending;
257 -};
258 -
259 -#define LAZY_QUEUE_INIT { { compare_commits_by_commit_date }, false }
260 -
261 -static void *lazy_queue_get(struct lazy_queue *queue)
262 -{
263 - if (queue->get_pending)
264 - prio_queue_get(&queue->queue);
265 - else
266 - queue->get_pending = true;
267 - return prio_queue_peek(&queue->queue);
268 -}
269 -
270 -static void lazy_queue_put(struct lazy_queue *queue, void *thing)
271 -{
272 - if (queue->get_pending)
273 - prio_queue_replace(&queue->queue, thing);
274 - else
275 - prio_queue_put(&queue->queue, thing);
276 - queue->get_pending = false;
277 -}
278 -
279 -static bool lazy_queue_empty(const struct lazy_queue *queue)
280 -{
281 - return prio_queue_size(&queue->queue) == (queue->get_pending ? 1 : 0);
282 -}
283 -
284 -static void lazy_queue_clear(struct lazy_queue *queue)
285 -{
286 - clear_prio_queue(&queue->queue);
287 - queue->get_pending = false;
288 -}
289 -
290 -static unsigned long finish_depth_computation(struct lazy_queue *queue,
254 +static unsigned long finish_depth_computation(struct prio_queue *queue,
255 struct possible_tag *best)
256 {
257 unsigned long seen_commits = 0;
258 struct oidset unflagged = OIDSET_INIT;
295 - struct commit *commit;
296 - int skip = queue->get_pending ? 1 : 0;
259 + struct commit *c;
260
298 - prio_queue_for_each(&queue->queue, commit) {
299 - if (skip) {
300 - skip = 0;
301 - continue;
302 - }
303 - if (!(commit->object.flags & best->flag_within))
304 - oidset_insert(&unflagged, &commit->object.oid);
261 + prio_queue_for_each(queue, c) {
262 + if (!(c->object.flags & best->flag_within))
263 + oidset_insert(&unflagged, &c->object.oid);
264 }
265
307 - while (!lazy_queue_empty(queue)) {
308 - struct commit *c = lazy_queue_get(queue);
266 + while ((c = prio_queue_get(queue))) {
267 struct commit_list *parents = c->parents;
268 seen_commits++;
269 if (c->object.flags & best->flag_within) {
@@ -321,7 +279,7 @@ static unsigned long finish_depth_computation(struct lazy_queue *queue,
279 repo_parse_commit(the_repository, p);
280 seen = p->object.flags & SEEN;
281 if (!seen)
324 - lazy_queue_put(queue, p);
282 + prio_queue_put(queue, p);
283 flag_before = p->object.flags & best->flag_within;
284 p->object.flags |= c->object.flags;
285 flag_after = p->object.flags & best->flag_within;
@@ -369,8 +327,8 @@ static void append_suffix(int depth, const struct object_id *oid, struct strbuf
327
328 static void describe_commit(struct commit *cmit, struct strbuf *dst)
329 {
372 - struct commit *gave_up_on = NULL;
373 - struct lazy_queue queue = LAZY_QUEUE_INIT;
330 + struct commit *c, *gave_up_on = NULL;
331 + struct prio_queue queue = { compare_commits_by_commit_date };
332 struct commit_name *n;
333 struct possible_tag all_matches[MAX_TAGS];
334 unsigned int match_cnt = 0, annotated_cnt = 0, cur_match;
@@ -412,9 +370,8 @@ static void describe_commit(struct commit *cmit, struct strbuf *dst)
370 }
371
372 cmit->object.flags = SEEN;
415 - lazy_queue_put(&queue, cmit);
416 - while (!lazy_queue_empty(&queue)) {
417 - struct commit *c = lazy_queue_get(&queue);
373 + prio_queue_put(&queue, cmit);
374 + while ((c = prio_queue_get(&queue))) {
375 struct commit_list *parents = c->parents;
376 struct commit_name **slot;
377
@@ -448,7 +405,7 @@ static void describe_commit(struct commit *cmit, struct strbuf *dst)
405 t->depth++;
406 }
407 /* Stop if last remaining path already covered by best candidate(s) */
451 - if (annotated_cnt && lazy_queue_empty(&queue)) {
408 + if (annotated_cnt && !prio_queue_size(&queue)) {
409 int best_depth = INT_MAX;
410 unsigned best_within = 0;
411 for (cur_match = 0; cur_match < match_cnt; cur_match++) {
@@ -471,7 +428,7 @@ static void describe_commit(struct commit *cmit, struct strbuf *dst)
428 struct commit *p = parents->item;
429 repo_parse_commit(the_repository, p);
430 if (!(p->object.flags & SEEN))
474 - lazy_queue_put(&queue, p);
431 + prio_queue_put(&queue, p);
432 p->object.flags |= c->object.flags;
433 parents = parents->next;
434
@@ -486,7 +443,7 @@ static void describe_commit(struct commit *cmit, struct strbuf *dst)
443 strbuf_add_unique_abbrev(dst, cmit_oid, abbrev);
444 if (suffix)
445 strbuf_addstr(dst, suffix);
489 - lazy_queue_clear(&queue);
446 + clear_prio_queue(&queue);
447 return;
448 }
449 if (unannotated_cnt)
@@ -502,11 +459,11 @@ static void describe_commit(struct commit *cmit, struct strbuf *dst)
459 QSORT(all_matches, match_cnt, compare_pt);
460
461 if (gave_up_on) {
505 - lazy_queue_put(&queue, gave_up_on);
462 + prio_queue_put(&queue, gave_up_on);
463 seen_commits--;
464 }
465 seen_commits += finish_depth_computation(&queue, &all_matches[0]);
509 - lazy_queue_clear(&queue);
466 + clear_prio_queue(&queue);
467
468 if (debug) {
469 static int label_width = -1;
builtin/show-branch.c
+3 -8
@@ -232,12 +232,13 @@ static void join_revs(struct prio_queue *queue,
232 while ((commit = prio_queue_peek(queue))) {
233 struct commit_list *parents;
234 int still_interesting = !!interesting(queue);
235 - bool get_pending = true;
235 int flags = commit->object.flags & all_mask;
236
237 if (!still_interesting && extra <= 0)
238 break;
239
240 + prio_queue_get(queue);
241 +
242 mark_seen(commit, seen_p);
243 if ((flags & all_revs) == all_revs)
244 flags |= UNINTERESTING;
@@ -253,14 +254,8 @@ static void join_revs(struct prio_queue *queue,
254 if (mark_seen(p, seen_p) && !still_interesting)
255 extra--;
256 p->object.flags |= flags;
256 - if (get_pending)
257 - prio_queue_replace(queue, p);
258 - else
259 - prio_queue_put(queue, p);
260 - get_pending = false;
257 + prio_queue_put(queue, p);
258 }
262 - if (get_pending)
263 - prio_queue_get(queue);
259 }
260
261 /*
commit.c
+2 -9
@@ -795,24 +795,17 @@ void commit_list_sort_by_date(struct commit_list **list)
795 struct commit *pop_most_recent_commit(struct prio_queue *queue,
796 unsigned int mark)
797 {
798 - struct commit *ret = prio_queue_peek(queue);
799 - int get_pending = 1;
798 + struct commit *ret = prio_queue_get(queue);
799 struct commit_list *parents = ret->parents;
800
801 while (parents) {
802 struct commit *commit = parents->item;
803 if (!repo_parse_commit(the_repository, commit) && !(commit->object.flags & mark)) {
804 commit->object.flags |= mark;
806 - if (get_pending)
807 - prio_queue_replace(queue, commit);
808 - else
809 - prio_queue_put(queue, commit);
810 - get_pending = 0;
805 + prio_queue_put(queue, commit);
806 }
807 parents = parents->next;
808 }
814 - if (get_pending)
815 - prio_queue_get(queue);
809 return ret;
810 }
811
prio-queue.c
+50 -42
@@ -34,12 +34,48 @@ void clear_prio_queue(struct prio_queue *queue)
34 queue->nr_ = 0;
35 queue->alloc = 0;
36 queue->insertion_ctr = 0;
37 + queue->get_pending = 0;
38 +}
39 +
40 +static void sift_down_root(struct prio_queue *queue)
41 +{
42 + size_t ix, child;
43 +
44 + /* Push down the one at the root */
45 + for (ix = 0; ix * 2 + 1 < queue->nr_; ix = child) {
46 + child = ix * 2 + 1; /* left */
47 + if (child + 1 < queue->nr_ &&
48 + compare(queue, child, child + 1) >= 0)
49 + child++; /* use right child */
50 +
51 + if (compare(queue, ix, child) <= 0)
52 + break;
53 +
54 + swap(queue, child, ix);
55 + }
56 +}
57 +
58 +static inline void flush_get(struct prio_queue *queue)
59 +{
60 + if (!queue->get_pending)
61 + return;
62 + queue->get_pending = 0;
63 + queue->array[0] = queue->array[--queue->nr_];
64 + sift_down_root(queue);
65 }
66
67 void prio_queue_put(struct prio_queue *queue, void *thing)
68 {
69 size_t ix, parent;
70
71 + if (queue->get_pending) {
72 + queue->get_pending = 0;
73 + queue->array[0].ctr = queue->insertion_ctr++;
74 + queue->array[0].data = thing;
75 + sift_down_root(queue);
76 + return;
77 + }
78 +
79 /* Append at the end */
80 ALLOC_GROW(queue->array, queue->nr_ + 1, queue->alloc);
81 queue->array[queue->nr_].ctr = queue->insertion_ctr++;
@@ -58,61 +94,33 @@ void prio_queue_put(struct prio_queue *queue, void *thing)
94 }
95 }
96
61 -static void sift_down_root(struct prio_queue *queue)
62 -{
63 - size_t ix, child;
64 -
65 - /* Push down the one at the root */
66 - for (ix = 0; ix * 2 + 1 < queue->nr_; ix = child) {
67 - child = ix * 2 + 1; /* left */
68 - if (child + 1 < queue->nr_ &&
69 - compare(queue, child, child + 1) >= 0)
70 - child++; /* use right child */
71 -
72 - if (compare(queue, ix, child) <= 0)
73 - break;
74 -
75 - swap(queue, child, ix);
76 - }
77 -}
78 -
97 void *prio_queue_get(struct prio_queue *queue)
98 {
81 - void *result;
82 -
83 - if (!queue->nr_)
99 + if (queue->nr_ <= queue->get_pending) {
100 + queue->nr_ = 0;
101 + queue->get_pending = 0;
102 return NULL;
103 + }
104 if (!queue->compare)
105 return queue->array[--queue->nr_].data; /* LIFO */
106
88 - result = queue->array[0].data;
89 - if (!--queue->nr_)
90 - return result;
107 + flush_get(queue);
108
92 - queue->array[0] = queue->array[queue->nr_];
93 - sift_down_root(queue);
94 - return result;
109 + queue->get_pending = 1;
110 + return queue->array[0].data;
111 }
112
113 void *prio_queue_peek(struct prio_queue *queue)
114 {
99 - if (!queue->nr_)
115 + if (queue->nr_ <= queue->get_pending) {
116 + queue->nr_ = 0;
117 + queue->get_pending = 0;
118 return NULL;
119 + }
120 if (!queue->compare)
121 return queue->array[queue->nr_ - 1].data;
103 - return queue->array[0].data;
104 -}
122
106 -void prio_queue_replace(struct prio_queue *queue, void *thing)
107 -{
108 - if (!queue->nr_) {
109 - prio_queue_put(queue, thing);
110 - } else if (!queue->compare) {
111 - queue->array[queue->nr_ - 1].ctr = queue->insertion_ctr++;
112 - queue->array[queue->nr_ - 1].data = thing;
113 - } else {
114 - queue->array[0].ctr = queue->insertion_ctr++;
115 - queue->array[0].data = thing;
116 - sift_down_root(queue);
117 - }
123 + flush_get(queue);
124 +
125 + return queue->array[0].data;
126 }
prio-queue.h
+4 -11
@@ -30,8 +30,9 @@ struct prio_queue {
30 prio_queue_compare_fn compare;
31 size_t insertion_ctr;
32 void *cb_data;
33 - size_t alloc, nr_;
33 + size_t alloc, nr_; /* use prio_queue_size() for logical count */
34 struct prio_queue_entry *array;
35 + unsigned get_pending;
36 };
37
38 /*
@@ -54,22 +55,14 @@ void *prio_queue_peek(struct prio_queue *);
55
56 static inline size_t prio_queue_size(const struct prio_queue *queue)
57 {
57 - return queue->nr_;
58 + return queue->nr_ - queue->get_pending;
59 }
60
61 #define prio_queue_for_each(queue, it) \
61 - for (size_t pq_ix_ = 0; \
62 + for (size_t pq_ix_ = (queue)->get_pending; \
63 pq_ix_ < (queue)->nr_ && ((it) = (queue)->array[pq_ix_].data, 1); \
64 pq_ix_++)
65
65 -/*
66 - * Replace the "thing" that compares the smallest with a new "thing",
67 - * like prio_queue_get()+prio_queue_put() would do, but in a more
68 - * efficient way. Does the same as prio_queue_put() if the queue is
69 - * empty.
70 - */
71 -void prio_queue_replace(struct prio_queue *queue, void *thing);
72 -
66 void clear_prio_queue(struct prio_queue *);
67
68 /* Reverse the LIFO elements */
t/unit-tests/u-prio-queue.c
+3 -3
@@ -53,13 +53,13 @@ static void test_prio_queue(int *input, size_t input_size,
53 prio_queue_reverse(&pq);
54 break;
55 case REPLACE:
56 - peek = prio_queue_peek(&pq);
56 + get = prio_queue_get(&pq);
57 cl_assert(i + 1 < input_size);
58 cl_assert(input[i + 1] >= 0);
59 cl_assert(j < result_size);
60 - cl_assert_equal_i(result[j], show(peek));
60 + cl_assert_equal_i(result[j], show(get));
61 j++;
62 - prio_queue_replace(&pq, &input[++i]);
62 + prio_queue_put(&pq, &input[++i]);
63 break;
64 default:
65 prio_queue_put(&pq, &input[i]);