commit-reach: use object flags for tips_reachable_from_bases()

tips_reachable_from_bases() walks the commit graph from a set of base commits to find which tip commits are reachable. The inner loop does a linear scan over the tips array to check whether each visited commit is a tip, making the overall cost O(C * T) where C is commits walked and T is the number of tips. Use the RESULT object flag to mark tip commits, replacing the linear scan with a single flag test per visited commit. This reduces the per-commit tip check from O(T) to O(1) and the overall cost from O(C * T) to O(C + T). When multiple refs point to the same commit, the shared object gets the flag once, so all duplicates are handled automatically. The early-termination advancement loop checks the flag on the sorted commits array directly, which naturally handles duplicates since the flag is on the shared commit object. This also removes the index field from struct commit_and_index, since the indirection through the original tips array is no longer needed. This function is called by `git for-each-ref --merged` and `git branch/tag --contains/--no-contains` via reach_filter() in ref-filter.c. Benchmark on a merge-heavy monorepo (2.3M commits, 10,000 refs): Command Before After Speedup for-each-ref --merged HEAD 6.57s 1.59s 4.1x for-each-ref --no-merged HEAD 6.67s 1.66s 4.0x branch --merged HEAD 0.68s 0.61s 10% branch --no-merged HEAD 0.65s 0.61s 8% tag --merged HEAD 0.12s 0.12s - On linux.git with 10,000 synthetic branches at the root commit (worst case for the DFS walk): Command Before After Speedup for-each-ref --merged HEAD 1.35s 0.35s 3.9x for-each-ref --no-merged HEAD 1.82s 0.31s 5.9x The large speedup for for-each-ref is because it checks all 10,000 refs as tips, making the O(T) inner loop expensive. The branch subcommand only checks local branches (fewer tips), so the improvement is smaller. Signed-off-by: Kristofer Karlsson <krka@spotify.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Kristofer Karlsson committed May 16, 2026 at 15:59 UTC b80b462d7cf2225c25959ce89727fdd3334c0afc
1 file changed +11 -12
commit-reach.c
+11 -12
@@ -1125,7 +1125,6 @@ void ahead_behind(struct repository *r,
1125
1126 struct commit_and_index {
1127 struct commit *commit;
1128 - unsigned int index;
1128 timestamp_t generation;
1129 };
1130
@@ -1165,7 +1164,6 @@ void tips_reachable_from_bases(struct repository *r,
1164
1165 for (size_t i = 0; i < tips_nr; i++) {
1166 commits[i].commit = tips[i];
1168 - commits[i].index = i;
1167 commits[i].generation = commit_graph_generation(tips[i]);
1168 }
1169
@@ -1173,6 +1171,9 @@ void tips_reachable_from_bases(struct repository *r,
1171 QSORT(commits, tips_nr, compare_commit_and_index_by_generation);
1172 min_generation = commits[0].generation;
1173
1174 + for (size_t i = 0; i < tips_nr; i++)
1175 + commits[i].commit->object.flags |= RESULT;
1176 +
1177 while (bases) {
1178 repo_parse_commit(r, bases->item);
1179 commit_list_insert(bases->item, &stack);
@@ -1183,20 +1184,16 @@ void tips_reachable_from_bases(struct repository *r,
1184 int explored_all_parents = 1;
1185 struct commit_list *p;
1186 struct commit *c = stack->item;
1186 - timestamp_t c_gen = commit_graph_generation(c);
1187
1188 /* Does it match any of our tips? */
1189 - for (size_t j = min_generation_index; j < tips_nr; j++) {
1190 - if (c_gen < commits[j].generation)
1191 - break;
1192 -
1193 - if (commits[j].commit == c) {
1194 - tips[commits[j].index]->object.flags |= mark;
1189 + {
1190 + if (c->object.flags & RESULT) {
1191 + c->object.flags |= mark;
1192
1196 - if (j == min_generation_index) {
1197 - unsigned int k = j + 1;
1193 + if (commits[min_generation_index].commit->object.flags & mark) {
1194 + unsigned int k = min_generation_index + 1;
1195 while (k < tips_nr &&
1199 - (tips[commits[k].index]->object.flags & mark))
1196 + (commits[k].commit->object.flags & mark))
1197 k++;
1198
1199 /* Terminate early if all found. */
@@ -1232,6 +1229,8 @@ void tips_reachable_from_bases(struct repository *r,
1229 }
1230
1231 done:
1232 + for (size_t i = 0; i < tips_nr; i++)
1233 + commits[i].commit->object.flags &= ~RESULT;
1234 free(commits);
1235 repo_clear_commit_marks(r, SEEN);
1236 commit_list_free(stack);