git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH 7/7] merge-ort, diffcore-rename: employ cached renames when possible

From
Elijah Newren via GitGitGadget <gitgitgadget@gmail.com>
Date
Mar 24, 2021, 21:32 UTC
Message-ID
<d1016c342d690b25da6f438bbcdd200917442531.1616621554.git.gitgitgadget@gmail.com>
In-Reply-To
<pull.859.git.1616621553.gitgitgadget@gmail.com>
From: Elijah Newren <newren@gmail.com>

When there are many renames between the old base of a series of commits and the new base, the way sequencer.c, merge-recursive.c, and diffcore-rename.c have traditionally split the work resulted in redetecting the same renames with each and every commit being transplanted. To address this, the last several commits have been creating a cache of rename detection results, determining when it was safe to use such a cache in subsequent merge operations, adding helper functions, and so on. See the previous half dozen commit messages for additional discussion of this optimization, particularly the message a few commits ago entitled "add code to check for whether cached renames can be reused". This commit finally ties all of that work together, modifying the merge algorithm to make use of these cached renames.

For the testcases mentioned in commit 557ac0350d ("merge-ort: begin performance work; instrument with trace2_region_* calls", 2020-10-28), this change improves the performance as follows:

                            Before                  After
    no-renames:        5.665 s ±  0.129 s     5.624 s ±  0.077 s
    mega-renames:     11.435 s ±  0.158 s    10.213 s ±  0.032 s
    just-one-mega:   494.2  ms ±  6.1  ms   497.6  ms ±  5.3  ms

That's a fairly small improvement, but mostly because the previous optimizations were so effective for these particular testcases; this optimization only kicks in when the others don't. If we undid the basename-guided rename detection and skip-irrelevant-renames optimizations, then we'd see that this series by itself improved performance as follows:

                   Before Basename Series   After Just This Series
    no-renames:      13.815 s ±  0.062 s      5.814 s ±  0.094 s
    mega-renames:  1799.937 s ±  0.493 s    303.225 s ±  1.330 s

Since this optimization kicks in to help accelerate cases where the previous optimizations do not apply, this last comparison shows that this cached-renames optimization has the potential to help signficantly in cases that don't meet the requirements for the other optimizations to be effective.

The changes made in this optimization also lay some important groundwork for a future optimization around having collect_merge_info() avoid recursing into subtrees in more cases.

Signed-off-by: Elijah Newren <newren@gmail.com>
---
 diffcore-rename.c | 22 ++++++++++++++++++----
 diffcore.h        |  3 ++-
 merge-ort.c       | 48 ++++++++++++++++++++++++++++++++++++++++++-----
 3 files changed, 63 insertions(+), 10 deletions(-)
diff --git a/diffcore-rename.c b/diffcore-rename.c
index 7cc24592617e..dfbe65c917e9 100644
--- a/diffcore-rename.c
+++ b/diffcore-rename.c
@@ -568,7 +568,8 @@ static void update_dir_rename_counts(struct dir_rename_info *info,
 static void initialize_dir_rename_info(struct dir_rename_info *info,
 				       struct strintmap *relevant_sources,
 				       struct strintmap *dirs_removed,
-				       struct strmap *dir_rename_count)
+				       struct strmap *dir_rename_count,
+				       struct strmap *cached_pairs)
 {
 	struct hashmap_iter iter;
 	struct strmap_entry *entry;
@@ -633,6 +634,17 @@ static void initialize_dir_rename_info(struct dir_rename_info *info,
 					 rename_dst[i].p->two->path);
 	}
 
+	/* Add cached_pairs to counts */
+	strmap_for_each_entry(cached_pairs, &iter, entry) {
+		const char *old_name = entry->key;
+		const char *new_name = entry->value;
+		if (!new_name)
+			/* known delete; ignore it */
+			continue;
+
+		update_dir_rename_counts(info, dirs_removed, old_name, new_name);
+	}
+
 	/*
 	 * Now we collapse
 	 *    dir_rename_count: old_directory -> {new_directory -> count}
@@ -1247,7 +1259,8 @@ static void handle_early_known_dir_renames(struct dir_rename_info *info,
 void diffcore_rename_extended(struct diff_options *options,
 			      struct strintmap *relevant_sources,
 			      struct strintmap *dirs_removed,
-			      struct strmap *dir_rename_count)
+			      struct strmap *dir_rename_count,
+			      struct strmap *cached_pairs)
 {
 	int detect_rename = options->detect_rename;
 	int minimum_score = options->rename_score;
@@ -1363,7 +1376,8 @@ void diffcore_rename_extended(struct diff_options *options,
 		/* Preparation for basename-driven matching. */
 		trace2_region_enter("diff", "dir rename setup", options->repo);
 		initialize_dir_rename_info(&info, relevant_sources,
-					   dirs_removed, dir_rename_count);
+					   dirs_removed, dir_rename_count,
+					   cached_pairs);
 		trace2_region_leave("diff", "dir rename setup", options->repo);
 
 		/* Utilize file basenames to quickly find renames. */
@@ -1561,5 +1575,5 @@ void diffcore_rename_extended(struct diff_options *options,
 
 void diffcore_rename(struct diff_options *options)
 {
-	diffcore_rename_extended(options, NULL, NULL, NULL);
+	diffcore_rename_extended(options, NULL, NULL, NULL, NULL);
 }
diff --git a/diffcore.h b/diffcore.h
index cf8d4cb8617d..de01e64becaf 100644
--- a/diffcore.h
+++ b/diffcore.h
@@ -181,7 +181,8 @@ void diffcore_rename(struct diff_options *);
 void diffcore_rename_extended(struct diff_options *options,
 			      struct strintmap *relevant_sources,
 			      struct strintmap *dirs_removed,
-			      struct strmap *dir_rename_count);
+			      struct strmap *dir_rename_count,
+			      struct strmap *cached_pairs);
 void diffcore_merge_broken(void);
 void diffcore_pickaxe(struct diff_options *);
 void diffcore_order(const char *orderfile);
diff --git a/merge-ort.c b/merge-ort.c
index 8d0353ffbca2..719222aa4364 100644
--- a/merge-ort.c
+++ b/merge-ort.c
@@ -752,15 +752,48 @@ static void add_pair(struct merge_options *opt,
 	struct rename_info *renames = &opt->priv->renames;
 	int names_idx = is_add ? side : 0;
 
-	if (!is_add) {
+	if (is_add) {
+		if (strset_contains(&renames->cached_target_names[side],
+				    pathname))
+			return;
+	} else {
 		unsigned content_relevant = (match_mask == 0);
 		unsigned location_relevant = (dir_rename_mask == 0x07);
 
+		/*
+		 * If pathname is found in cached_irrelevant[side] due to
+		 * previous pick but for this commit content is relevant,
+		 * then we need to remove it from cached_irrelevant.
+		 */
+		if (content_relevant)
+			/* strset_remove is no-op if strset doesn't have key */
+			strset_remove(&renames->cached_irrelevant[side],
+				      pathname);
+
+		/*
+		 * We do not need to re-detect renames for paths that we already
+		 * know the pairing, i.e. for cached_pairs (or
+		 * cached_irrelevant).  However, handle_deferred_entries() needs
+		 * to loop over the union of keys from relevant_sources[side] and
+		 * cached_pairs[side], so for simplicity we set relevant_sources
+		 * for all the cached_pairs too and then strip them back out in
+		 * prune_cached_from_relevant() at the beginning of
+		 * detect_regular_renames().
+		 */
 		if (content_relevant || location_relevant) {
 			/* content_relevant trumps location_relevant */
 			strintmap_set(&renames->relevant_sources[side], pathname,
 				      content_relevant ? RELEVANT_CONTENT : RELEVANT_LOCATION);
 		}
+
+		/*
+		 * Avoid creating pair if we've already cached rename results.
+		 * Note that we do this after setting relevant_sources[side]
+		 * as noted in the comment above.
+		 */
+		if (strmap_contains(&renames->cached_pairs[side], pathname) ||
+		    strset_contains(&renames->cached_irrelevant[side], pathname))
+			return;
 	}
 
 	one = alloc_filespec(pathname);
@@ -2336,7 +2369,9 @@ static inline int possible_side_renames(struct rename_info *renames,
 static inline int possible_renames(struct rename_info *renames)
 {
 	return possible_side_renames(renames, 1) ||
-	       possible_side_renames(renames, 2);
+	       possible_side_renames(renames, 2) ||
+	       !strmap_empty(&renames->cached_pairs[1]) ||
+	       !strmap_empty(&renames->cached_pairs[2]);
 }
 
 static void resolve_diffpair_statuses(struct diff_queue_struct *q)
@@ -2360,7 +2395,6 @@ static void resolve_diffpair_statuses(struct diff_queue_struct *q)
 	}
 }
 
-MAYBE_UNUSED
 static void prune_cached_from_relevant(struct rename_info *renames,
 				       unsigned side)
 {
@@ -2380,7 +2414,6 @@ static void prune_cached_from_relevant(struct rename_info *renames,
 	}
 }
 
-MAYBE_UNUSED
 static void use_cached_pairs(struct merge_options *opt,
 			     struct strmap *cached_pairs,
 			     struct diff_queue_struct *pairs)
@@ -2463,6 +2496,7 @@ static void detect_regular_renames(struct merge_options *opt,
 	struct diff_options diff_opts;
 	struct rename_info *renames = &opt->priv->renames;
 
+	prune_cached_from_relevant(renames, side_index);
 	if (!possible_side_renames(renames, side_index)) {
 		/*
 		 * No rename detection needed for this side, but we still need
@@ -2473,6 +2507,7 @@ static void detect_regular_renames(struct merge_options *opt,
 		return;
 	}
 
+	partial_clear_dir_rename_count(&renames->dir_rename_count[side_index]);
 	repo_diff_setup(opt->repo, &diff_opts);
 	diff_opts.flags.recursive = 1;
 	diff_opts.flags.rename_empty = 0;
@@ -2490,7 +2525,8 @@ static void detect_regular_renames(struct merge_options *opt,
 	diffcore_rename_extended(&diff_opts,
 				 &renames->relevant_sources[side_index],
 				 &renames->dirs_removed[side_index],
-				 &renames->dir_rename_count[side_index]);
+				 &renames->dir_rename_count[side_index],
+				 &renames->cached_pairs[side_index]);
 	trace2_region_leave("diff", "diffcore_rename", opt->repo);
 	resolve_diffpair_statuses(&diff_queued_diff);
 
@@ -2597,6 +2633,8 @@ static int detect_and_process_renames(struct merge_options *opt,
 	trace2_region_enter("merge", "regular renames", opt->repo);
 	detect_regular_renames(opt, MERGE_SIDE1);
 	detect_regular_renames(opt, MERGE_SIDE2);
+	use_cached_pairs(opt, &renames->cached_pairs[1], &renames->pairs[1]);
+	use_cached_pairs(opt, &renames->cached_pairs[2], &renames->pairs[2]);
 	trace2_region_leave("merge", "regular renames", opt->repo);
 
 	trace2_region_enter("merge", "directory renames", opt->repo);
-- 
gitgitgadget
Previous: Elijah Newren via GitGitGadgetNext: Junio C Hamano
Message 8 of 61 in “Optimization batch 11: avoid repeatedly detecting same renames”
  1. 0/7 Optimization batch 11: avoid repeatedly detecting same renamesElijah Newren via GitGitGadget, Mar 24, 2021
  2. 2/7 merge-ort: populate caches of rename detection resultsElijah Newren via GitGitGadget, Mar 24, 2021
  3. 1/7 merge-ort: add data structures for in-memory caching of rename detectionElijah Newren via GitGitGadget, Mar 24, 2021
  4. 4/7 merge-ort: avoid accidental API mis-useElijah Newren via GitGitGadget, Mar 24, 2021
  5. 3/7 merge-ort: add code to check for whether cached renames can be reusedElijah Newren via GitGitGadget, Mar 24, 2021
  6. 5/7 merge-ort: preserve cached renames for the appropriate sideElijah Newren via GitGitGadget, Mar 24, 2021
  7. 6/7 merge-ort: add helper functions for using cached renamesElijah Newren via GitGitGadget, Mar 24, 2021
  8. 7/7 merge-ort, diffcore-rename: employ cached renames when possibleElijah Newren via GitGitGadget, Mar 24, 2021
  9. Junio C HamanoMar 24, 2021
  10. Elijah NewrenMar 24, 2021
  11. Junio C HamanoMar 25, 2021
  12. Elijah NewrenMar 29, 2021
  13. Derrick StoleeMar 30, 2021
  14. 00/13 Optimization batch 11: avoid repeatedly detecting same renamesElijah Newren via GitGitGadget, May 4, 2021
  15. 01/13 t6423: rename file within directory that other side renamedElijah Newren via GitGitGadget, May 4, 2021
  16. 03/13 fast-rebase: change assert() to BUG()Elijah Newren via GitGitGadget, May 4, 2021
  17. 04/13 fast-rebase: write conflict state to working tree, index, and HEADElijah Newren via GitGitGadget, May 4, 2021
  18. Derrick StoleeMay 17, 2021
  19. Elijah NewrenMay 18, 2021
  20. Derrick StoleeMay 18, 2021
  21. 02/13 Documentation/technical: describe remembering renames optimizationElijah Newren via GitGitGadget, May 4, 2021
  22. 07/13 merge-ort: populate caches of rename detection resultsElijah Newren via GitGitGadget, May 4, 2021
  23. Derrick StoleeMay 17, 2021
  24. Elijah NewrenMay 20, 2021
  25. 06/13 merge-ort: add data structures for in-memory caching of rename detectionElijah Newren via GitGitGadget, May 4, 2021
  26. Derrick StoleeMay 17, 2021
  27. Elijah NewrenMay 18, 2021
  28. Derrick StoleeMay 18, 2021
  29. 05/13 t6429: testcases for remembering renamesElijah Newren via GitGitGadget, May 4, 2021
  30. 08/13 merge-ort: add code to check for whether cached renames can be reusedElijah Newren via GitGitGadget, May 4, 2021
  31. Derrick StoleeMay 17, 2021
  32. 10/13 merge-ort: preserve cached renames for the appropriate sideElijah Newren via GitGitGadget, May 4, 2021
  33. 11/13 merge-ort: add helper functions for using cached renamesElijah Newren via GitGitGadget, May 4, 2021
  34. 12/13 merge-ort: handle interactions of caching and rename/rename(1to1) casesElijah Newren via GitGitGadget, May 4, 2021
  35. Derrick StoleeMay 17, 2021
  36. 13/13 merge-ort, diffcore-rename: employ cached renames when possibleElijah Newren via GitGitGadget, May 4, 2021
  37. Derrick StoleeMay 17, 2021
  38. Elijah NewrenMay 20, 2021
  39. Derrick StoleeMay 22, 2021
  40. 09/13 merge-ort: avoid accidental API mis-useElijah Newren via GitGitGadget, May 4, 2021
  41. Derrick StoleeMay 17, 2021
  42. Elijah NewrenMay 14, 2021
  43. Derrick StoleeMay 14, 2021
  44. 00/13 Optimization batch 11: avoid repeatedly detecting same renamesElijah Newren via GitGitGadget, May 20, 2021
  45. 01/13 t6423: rename file within directory that other side renamedElijah Newren via GitGitGadget, May 20, 2021
  46. 02/13 Documentation/technical: describe remembering renames optimizationElijah Newren via GitGitGadget, May 20, 2021
  47. Bagas SanjayaMay 20, 2021
  48. Kerry, RichardMay 20, 2021
  49. Elijah NewrenMay 20, 2021
  50. 03/13 fast-rebase: change assert() to BUG()Elijah Newren via GitGitGadget, May 20, 2021
  51. 04/13 fast-rebase: write conflict state to working tree, index, and HEADElijah Newren via GitGitGadget, May 20, 2021
  52. 05/13 t6429: testcases for remembering renamesElijah Newren via GitGitGadget, May 20, 2021
  53. 08/13 merge-ort: add code to check for whether cached renames can be reusedElijah Newren via GitGitGadget, May 20, 2021
  54. 07/13 merge-ort: populate caches of rename detection resultsElijah Newren via GitGitGadget, May 20, 2021
  55. 06/13 merge-ort: add data structures for in-memory caching of rename detectionElijah Newren via GitGitGadget, May 20, 2021
  56. 09/13 merge-ort: avoid accidental API mis-useElijah Newren via GitGitGadget, May 20, 2021
  57. 10/13 merge-ort: preserve cached renames for the appropriate sideElijah Newren via GitGitGadget, May 20, 2021
  58. 12/13 merge-ort: handle interactions of caching and rename/rename(1to1) casesElijah Newren via GitGitGadget, May 20, 2021
  59. 11/13 merge-ort: add helper functions for using cached renamesElijah Newren via GitGitGadget, May 20, 2021
  60. 13/13 merge-ort, diffcore-rename: employ cached renames when possibleElijah Newren via GitGitGadget, May 20, 2021
  61. Derrick StoleeMay 22, 2021

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.