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