Skip to content

Commit 134ec33

Browse files
rscharfegitster
authored andcommitted
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>
1 parent 81f86aa commit 134ec33

File tree

2 files changed

+110
-5
lines changed

2 files changed

+110
-5
lines changed

commit-reach.c

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ static int paint_down_to_common(struct repository *r,
6060
struct prio_queue queue = { compare_commits_by_gen_then_commit_date };
6161
int i;
6262
timestamp_t last_gen = GENERATION_NUMBER_INFINITY;
63+
struct commit_list **tail = result;
6364

6465
if (!min_generation && !corrected_commit_dates_enabled(r))
6566
queue.compare = compare_commits_by_commit_date;
@@ -95,7 +96,7 @@ static int paint_down_to_common(struct repository *r,
9596
if (flags == (PARENT1 | PARENT2)) {
9697
if (!(commit->object.flags & RESULT)) {
9798
commit->object.flags |= RESULT;
98-
commit_list_insert_by_date(commit, result);
99+
tail = commit_list_append(commit, tail);
99100
}
100101
/* Mark parents of a found merge stale */
101102
flags |= STALE;
@@ -128,6 +129,7 @@ static int paint_down_to_common(struct repository *r,
128129
}
129130

130131
clear_prio_queue(&queue);
132+
commit_list_sort_by_date(result);
131133
return 0;
132134
}
133135

@@ -136,7 +138,7 @@ static int merge_bases_many(struct repository *r,
136138
struct commit **twos,
137139
struct commit_list **result)
138140
{
139-
struct commit_list *list = NULL;
141+
struct commit_list *list = NULL, **tail = result;
140142
int i;
141143

142144
for (i = 0; i < n; i++) {
@@ -171,8 +173,9 @@ static int merge_bases_many(struct repository *r,
171173
while (list) {
172174
struct commit *commit = pop_commit(&list);
173175
if (!(commit->object.flags & STALE))
174-
commit_list_insert_by_date(commit, result);
176+
tail = commit_list_append(commit, tail);
175177
}
178+
commit_list_sort_by_date(result);
176179
return 0;
177180
}
178181

@@ -425,7 +428,7 @@ static int get_merge_bases_many_0(struct repository *r,
425428
int cleanup,
426429
struct commit_list **result)
427430
{
428-
struct commit_list *list;
431+
struct commit_list *list, **tail = result;
429432
struct commit **rslt;
430433
size_t cnt, i;
431434
int ret;
@@ -461,7 +464,8 @@ static int get_merge_bases_many_0(struct repository *r,
461464
return -1;
462465
}
463466
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);
465469
free(rslt);
466470
return 0;
467471
}

t/perf/p6010-merge-base.sh

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

0 commit comments

Comments
 (0)