Skip to content

Commit 59c4c7d

Browse files
peffgitster
authored andcommitted
tree-walk: reduce stack size for recursive functions
The traverse_trees() and traverse_trees_recursive() functions call each other recursively. In a deep tree, this can result in running out of stack space and crashing. There's obviously going to be some limit here based on available stack, but the problem is exacerbated by a few large structs, many of which we over-allocate. For example, in traverse_trees() we store a name_entry and tree_desc_x per tree, both of which contain an object_id (which is now 32 bytes). And we allocate 8 of them (from MAX_TRAVERSE_TREES), even though many traversals will only look at 1 or 2. Interestingly, we used to allocate these on the heap, prior to 8dd40c0 (traverse_trees(): use stack array for name entries, 2020-01-30). That commit was trying to simplify away allocation size computations, and naively assumed that the sizes were small enough not to matter. And they don't in normal cases, but on my stock Debian system I see a crash running "git archive" on a tree with ~3600 entries. That's deep enough we wouldn't see it in practice, but probably shallow enough that we'd prefer not to make it a hard limit. Especially because other systems may have even smaller stacks. We can replace these stack variables with a few malloc invocations. This reduces the stack sizes for the two functions from 1128 and 752 bytes, respectively, down to 40 and 92 bytes. That allows a depth of ~13000 on my machine (the improvement isn't in linear proportion because my numbers don't count the size of parameters and other function overhead). The possible downsides are: 1. We now have to remember to free(). But both functions have an easy single exit (and already had to clean up other bits anyway). 2. The extra malloc()/free() overhead might be measurable. I tested this by setting up a 3000-depth tree with a single blob and running "git archive" on it. After switching to the heap, it consistently runs 2-3% faster! Presumably this is because the 1K+ of wasted stack space penalized memory caches. On a more real-world case like linux.git, the speed difference isn't measurable at all, simply because most trees aren't that deep and there's so much other work going on (like accessing the objects themselves). So the improvement I saw should be taken as evidence that we're not making anything worse, but isn't really that interesting on its own. The main motivation here is that we're now less likely to run out of stack space and crash. Signed-off-by: Jeff King <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 43c8a30 commit 59c4c7d

File tree

2 files changed

+13
-6
lines changed

2 files changed

+13
-6
lines changed

tree-walk.c

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -442,9 +442,9 @@ int traverse_trees(struct index_state *istate,
442442
struct traverse_info *info)
443443
{
444444
int error = 0;
445-
struct name_entry entry[MAX_TRAVERSE_TREES];
445+
struct name_entry *entry;
446446
int i;
447-
struct tree_desc_x tx[ARRAY_SIZE(entry)];
447+
struct tree_desc_x *tx;
448448
struct strbuf base = STRBUF_INIT;
449449
int interesting = 1;
450450
char *traverse_path;
@@ -455,8 +455,8 @@ int traverse_trees(struct index_state *istate,
455455
if (traverse_trees_cur_depth > traverse_trees_max_depth)
456456
traverse_trees_max_depth = traverse_trees_cur_depth;
457457

458-
if (n >= ARRAY_SIZE(entry))
459-
BUG("traverse_trees() called with too many trees (%d)", n);
458+
ALLOC_ARRAY(entry, n);
459+
ALLOC_ARRAY(tx, n);
460460

461461
for (i = 0; i < n; i++) {
462462
tx[i].d = t[i];
@@ -551,6 +551,8 @@ int traverse_trees(struct index_state *istate,
551551
}
552552
for (i = 0; i < n; i++)
553553
free_extended_entry(tx + i);
554+
free(tx);
555+
free(entry);
554556
free(traverse_path);
555557
info->traverse_path = NULL;
556558
strbuf_release(&base);

unpack-trees.c

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -864,8 +864,8 @@ static int traverse_trees_recursive(int n, unsigned long dirmask,
864864
struct unpack_trees_options *o = info->data;
865865
int i, ret, bottom;
866866
int nr_buf = 0;
867-
struct tree_desc t[MAX_UNPACK_TREES];
868-
void *buf[MAX_UNPACK_TREES];
867+
struct tree_desc *t;
868+
void **buf;
869869
struct traverse_info newinfo;
870870
struct name_entry *p;
871871
int nr_entries;
@@ -902,6 +902,9 @@ static int traverse_trees_recursive(int n, unsigned long dirmask,
902902
newinfo.pathlen = st_add3(newinfo.pathlen, tree_entry_len(p), 1);
903903
newinfo.df_conflicts |= df_conflicts;
904904

905+
ALLOC_ARRAY(t, n);
906+
ALLOC_ARRAY(buf, n);
907+
905908
/*
906909
* Fetch the tree from the ODB for each peer directory in the
907910
* n commits.
@@ -937,6 +940,8 @@ static int traverse_trees_recursive(int n, unsigned long dirmask,
937940

938941
for (i = 0; i < nr_buf; i++)
939942
free(buf[i]);
943+
free(buf);
944+
free(t);
940945

941946
return ret;
942947
}

0 commit comments

Comments
 (0)