commit-reach: avoid commit_list_insert_by_date()

Building a list using commit_list_insert_by_date() has quadratic worst case complexity. Avoid it by just appending in the loop and sorting at the end. The number of merge bases is usually small, so don't expect speedups in normal repositories. It has no limit, though. The added perf test shows a nice improvement when dealing with 16384 merge bases: Test v2.51.1 HEAD ----------------------------------------------------------------- 6010.2: git merge-base 0.55(0.54+0.00) 0.03(0.02+0.00) -94.5% Signed-off-by: René Scharfe <l.s.r@web.de> Signed-off-by: Junio C Hamano <gitster@pobox.com>

René Scharfe committed Oct 24, 2025 at 18:47 UTC 134ec330d2945002d0ceb7de2ac6cd7ab0af762d
2 files changed +110 -5
commit-reach.c
+9 -5
@@ -60,6 +60,7 @@ static int paint_down_to_common(struct repository *r,
60 struct prio_queue queue = { compare_commits_by_gen_then_commit_date };
61 int i;
62 timestamp_t last_gen = GENERATION_NUMBER_INFINITY;
63 + struct commit_list **tail = result;
64
65 if (!min_generation && !corrected_commit_dates_enabled(r))
66 queue.compare = compare_commits_by_commit_date;
@@ -95,7 +96,7 @@ static int paint_down_to_common(struct repository *r,
96 if (flags == (PARENT1 | PARENT2)) {
97 if (!(commit->object.flags & RESULT)) {
98 commit->object.flags |= RESULT;
98 - commit_list_insert_by_date(commit, result);
99 + tail = commit_list_append(commit, tail);
100 }
101 /* Mark parents of a found merge stale */
102 flags |= STALE;
@@ -128,6 +129,7 @@ static int paint_down_to_common(struct repository *r,
129 }
130
131 clear_prio_queue(&queue);
132 + commit_list_sort_by_date(result);
133 return 0;
134 }
135
@@ -136,7 +138,7 @@ static int merge_bases_many(struct repository *r,
138 struct commit **twos,
139 struct commit_list **result)
140 {
139 - struct commit_list *list = NULL;
141 + struct commit_list *list = NULL, **tail = result;
142 int i;
143
144 for (i = 0; i < n; i++) {
@@ -171,8 +173,9 @@ static int merge_bases_many(struct repository *r,
173 while (list) {
174 struct commit *commit = pop_commit(&list);
175 if (!(commit->object.flags & STALE))
174 - commit_list_insert_by_date(commit, result);
176 + tail = commit_list_append(commit, tail);
177 }
178 + commit_list_sort_by_date(result);
179 return 0;
180 }
181
@@ -425,7 +428,7 @@ static int get_merge_bases_many_0(struct repository *r,
428 int cleanup,
429 struct commit_list **result)
430 {
428 - struct commit_list *list;
431 + struct commit_list *list, **tail = result;
432 struct commit **rslt;
433 size_t cnt, i;
434 int ret;
@@ -461,7 +464,8 @@ static int get_merge_bases_many_0(struct repository *r,
464 return -1;
465 }
466 for (i = 0; i < cnt; i++)
464 - commit_list_insert_by_date(rslt[i], result);
467 + tail = commit_list_append(rslt[i], tail);
468 + commit_list_sort_by_date(result);
469 free(rslt);
470 return 0;
471 }
t/perf/p6010-merge-base.sh new
+101
@@ -0,0 +1,101 @@
1 +#!/bin/sh
2 +
3 +test_description='Test git merge-base'
4 +
5 +. ./perf-lib.sh
6 +
7 +test_perf_fresh_repo
8 +
9 +#
10 +# Creates lots of merges to make history traversal costly. In
11 +# particular it creates 2^($max_level-1)-1 2-way merges on top of
12 +# 2^($max_level-1) root commits. E.g., the commit history looks like
13 +# this for a $max_level of 3:
14 +#
15 +# _1_
16 +# / \
17 +# 2 3
18 +# / \ / \
19 +# 4 5 6 7
20 +#
21 +# The numbers are the fast-import marks, which also are the commit
22 +# messages. 1 is the HEAD commit and a merge, 2 and 3 are also merges,
23 +# 4-7 are the root commits.
24 +#
25 +build_history () {
26 + local max_level="$1" &&
27 + local level="${2:-1}" &&
28 + local mark="${3:-1}" &&
29 + if test $level -eq $max_level
30 + then
31 + echo "reset refs/heads/master" &&
32 + echo "from $ZERO_OID" &&
33 + echo "commit refs/heads/master" &&
34 + echo "mark :$mark" &&
35 + echo "committer C <c@example.com> 1234567890 +0000" &&
36 + echo "data <<EOF" &&
37 + echo "$mark" &&
38 + echo "EOF"
39 + else
40 + local level1=$((level+1)) &&
41 + local mark1=$((2*mark)) &&
42 + local mark2=$((2*mark+1)) &&
43 + build_history $max_level $level1 $mark1 &&
44 + build_history $max_level $level1 $mark2 &&
45 + echo "commit refs/heads/master" &&
46 + echo "mark :$mark" &&
47 + echo "committer C <c@example.com> 1234567890 +0000" &&
48 + echo "data <<EOF" &&
49 + echo "$mark" &&
50 + echo "EOF" &&
51 + echo "from :$mark1" &&
52 + echo "merge :$mark2"
53 + fi
54 +}
55 +
56 +#
57 +# Creates a new merge history in the same shape as build_history does,
58 +# while reusing the same root commits. This way the two top commits
59 +# have 2^($max_level-1) merge bases between them.
60 +#
61 +build_history2 () {
62 + local max_level="$1" &&
63 + local level="${2:-1}" &&
64 + local mark="${3:-1}" &&
65 + if test $level -lt $max_level
66 + then
67 + local level1=$((level+1)) &&
68 + local mark1=$((2*mark)) &&
69 + local mark2=$((2*mark+1)) &&
70 + build_history2 $max_level $level1 $mark1 &&
71 + build_history2 $max_level $level1 $mark2 &&
72 + echo "commit refs/heads/master" &&
73 + echo "mark :$mark" &&
74 + echo "committer C <c@example.com> 1234567890 +0000" &&
75 + echo "data <<EOF" &&
76 + echo "$mark II" &&
77 + echo "EOF" &&
78 + echo "from :$mark1" &&
79 + echo "merge :$mark2"
80 + fi
81 +}
82 +
83 +test_expect_success 'setup' '
84 + max_level=15 &&
85 + build_history $max_level | git fast-import --export-marks=marks &&
86 + git tag one &&
87 + build_history2 $max_level | git fast-import --import-marks=marks --force &&
88 + git tag two &&
89 + git gc &&
90 + git log --format=%H --no-merges >expect
91 +'
92 +
93 +test_perf 'git merge-base' '
94 + git merge-base --all one two >actual
95 +'
96 +
97 +test_expect_success 'verify result' '
98 + test_cmp expect actual
99 +'
100 +
101 +test_done