Skip to content

Commit a8dda1a

Browse files
peffgitster
authored andcommitted
tree-diff: drop path_appendnew() alloc optimization
When we're diffing trees, we create a list of combine_diff_path structs that represent changed paths. We allocate each struct and add it to the list with path_appendnew(), which we then feed to opt->pathchange(). That function tells us whether the path is of interest or not; if not, then we can throw away the struct we allocated. So there's an optimization to avoid extra allocations: instead of throwing away the new entry, we try to reuse it. If it was large enough to store the next path we care about, we can do so. And if not, we fall back to freeing and re-allocating a new struct. This comes from 72441af (tree-diff: rework diff_tree() to generate diffs for multiparent cases as well, 2014-04-07), where the goal was to have even the 2-parent diff code use the combine-diff infrastructure, but without taking a performance hit. The implementation causes some complexities in the interface (as we store the allocation length inside the "next" pointer), and prevents us from using the regular combine_diff_path_new() constructor. The complexity is mostly contained inside two functions, but it's worth re-evaluating how much it's helping. That commit claims it helps ~1% on generating two-parent diffs in linux.git. Here are the timings I get on the same command today ("old" is the current tip of master, and "new" has this patch applied): Benchmark 1: ./git.old log --raw --no-abbrev --no-renames v3.10..v3.11 Time (mean ± σ): 532.9 ms ± 5.8 ms [User: 472.7 ms, System: 59.6 ms] Range (min … max): 525.9 ms … 543.3 ms 10 runs Benchmark 2: ./git.new log --raw --no-abbrev --no-renames v3.10..v3.11 Time (mean ± σ): 538.3 ms ± 5.7 ms [User: 478.0 ms, System: 59.7 ms] Range (min … max): 528.5 ms … 545.3 ms 10 runs Summary ./git.old log --raw --no-abbrev --no-renames v3.10..v3.11 ran 1.01 ± 0.02 times faster than ./git.new log --raw --no-abbrev --no-renames v3.10..v3.11 So we do end up on average 1% faster, but with 2% of noise. I tried to focus more on diff performance by running the commit traversal separately, like: git rev-list v3.10..v3.11 >in and then timing just the diffs: Benchmark 1: ./git.old diff-tree --stdin -r <in Time (mean ± σ): 415.7 ms ± 5.8 ms [User: 357.7 ms, System: 58.0 ms] Range (min … max): 410.9 ms … 430.3 ms 10 runs Benchmark 2: ./git.new diff-tree --stdin -r <in Time (mean ± σ): 418.5 ms ± 2.1 ms [User: 361.7 ms, System: 56.6 ms] Range (min … max): 414.9 ms … 421.3 ms 10 runs Summary ./git.old diff-tree --stdin -r <in ran 1.01 ± 0.02 times faster than ./git.new diff-tree --stdin -r <in That gets roughly the same result. Adding in "-c" to do multi-parent diffs doesn't change much: Benchmark 1: ./git.old diff-tree --stdin -r -c <in Time (mean ± σ): 525.3 ms ± 6.6 ms [User: 470.0 ms, System: 55.1 ms] Range (min … max): 508.4 ms … 531.0 ms 10 runs Benchmark 2: ./git.new diff-tree --stdin -r -c <in Time (mean ± σ): 532.3 ms ± 6.2 ms [User: 469.0 ms, System: 63.1 ms] Range (min … max): 520.3 ms … 539.4 ms 10 runs Summary ./git.old diff-tree --stdin -r -c <in ran 1.01 ± 0.02 times faster than ./git.new diff-tree --stdin -r -c <in And of course if you add in a lot more work by doing actual content-level diffs, any difference is lost entirely (here the newer version is actually faster, but that's really just noise): Benchmark 1: ./git.old diff-tree --stdin -r --cc <in Time (mean ± σ): 11.571 s ± 0.064 s [User: 11.287 s, System: 0.283 s] Range (min … max): 11.497 s … 11.615 s 3 runs Benchmark 2: ./git.new diff-tree --stdin -r --cc <in Time (mean ± σ): 11.466 s ± 0.109 s [User: 11.108 s, System: 0.357 s] Range (min … max): 11.346 s … 11.560 s 3 runs Summary ./git.new diff-tree --stdin -r --cc <in ran 1.01 ± 0.01 times faster than ./git.old diff-tree --stdin -r --cc <in So my conclusion is that it probably does help a little, but it's mostly lost in the noise. I could see an argument for keeping it, as the complexity is hidden away in functions that do not often need to be touched. But it does make them more confusing than necessary (despite some detailed explanations from the author of that commit; it just took me a while to wrap my head around what was going on) and prevents further refactoring of the combine_diff_path struct. So let's drop it. Signed-off-by: Jeff King <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent ca3abe4 commit a8dda1a

File tree

1 file changed

+6
-61
lines changed

1 file changed

+6
-61
lines changed

tree-diff.c

Lines changed: 6 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -127,30 +127,6 @@ static int emit_diff_first_parent_only(struct diff_options *opt, struct combine_
127127
/*
128128
* Make a new combine_diff_path from path/mode/sha1
129129
* and append it to paths list tail.
130-
*
131-
* Memory for created elements could be reused:
132-
*
133-
* - if last->next == NULL, the memory is allocated;
134-
*
135-
* - if last->next != NULL, it is assumed that p=last->next was returned
136-
* earlier by this function, and p->next was *not* modified.
137-
* The memory is then reused from p.
138-
*
139-
* so for clients,
140-
*
141-
* - if you do need to keep the element
142-
*
143-
* p = path_appendnew(p, ...);
144-
* process(p);
145-
* p->next = NULL;
146-
*
147-
* - if you don't need to keep the element after processing
148-
*
149-
* pprev = p;
150-
* p = path_appendnew(p, ...);
151-
* process(p);
152-
* p = pprev;
153-
* ; don't forget to free tail->next in the end
154130
*/
155131
static struct combine_diff_path *path_appendnew(struct combine_diff_path *last,
156132
int nparent, const struct strbuf *base, const char *path, int pathlen,
@@ -160,22 +136,8 @@ static struct combine_diff_path *path_appendnew(struct combine_diff_path *last,
160136
size_t len = st_add(base->len, pathlen);
161137
size_t alloclen = combine_diff_path_size(nparent, len);
162138

163-
/* if last->next is !NULL - it is a pre-allocated memory, we can reuse */
164-
p = last->next;
165-
if (p && (alloclen > (intptr_t)p->next)) {
166-
FREE_AND_NULL(p);
167-
}
168-
169-
if (!p) {
170-
p = xmalloc(alloclen);
171-
172-
/*
173-
* until we go to it next round, .next holds how many bytes we
174-
* allocated (for faster realloc - we don't need copying old data).
175-
*/
176-
p->next = (struct combine_diff_path *)(intptr_t)alloclen;
177-
}
178-
139+
p = xmalloc(alloclen);
140+
p->next = NULL;
179141
last->next = p;
180142

181143
p->path = (char *)&(p->parent[nparent]);
@@ -279,21 +241,11 @@ static struct combine_diff_path *emit_path(struct combine_diff_path *p,
279241
if (opt->pathchange)
280242
keep = opt->pathchange(opt, p);
281243

282-
/*
283-
* If a path was filtered or consumed - we don't need to add it
284-
* to the list and can reuse its memory, leaving it as
285-
* pre-allocated element on the tail.
286-
*
287-
* On the other hand, if path needs to be kept, we need to
288-
* correct its .next to NULL, as it was pre-initialized to how
289-
* much memory was allocated.
290-
*
291-
* see path_appendnew() for details.
292-
*/
293-
if (!keep)
244+
if (!keep) {
245+
free(p);
246+
pprev->next = NULL;
294247
p = pprev;
295-
else
296-
p->next = NULL;
248+
}
297249
}
298250

299251
if (recurse) {
@@ -585,13 +537,6 @@ struct combine_diff_path *diff_tree_paths(
585537
struct strbuf *base, struct diff_options *opt)
586538
{
587539
p = ll_diff_tree_paths(p, oid, parents_oid, nparent, base, opt, 0);
588-
589-
/*
590-
* free pre-allocated last element, if any
591-
* (see path_appendnew() for details about why)
592-
*/
593-
FREE_AND_NULL(p->next);
594-
595540
return p;
596541
}
597542

0 commit comments

Comments
 (0)