read-cache: speed up add_index_entry
v9 cover read-cache: speed up add_index_entry
From: Jeff Hostetler <[email protected]>
Version 9 addresses the following:
() p0006 perf test can now run using either synthetic repos
from t/perf/repos/many-repos.sh -OR- an actual real-world repo.
() The commit message has been updated to include results of
p0006 on linux.git.
() Line 0006.3 shows a positive value, but results for that
line had a lot of variance between runs. I measured the blocks of code I added and they only added 0.007 to 0.015 seconds. I suspect the overall time difference and variance is due to file I/O to update the worktree when switching branches.
I think this version has addressed everything raise so far, so I think I'm ready to let this one rest. Thanks for all the help and feedback.
Jeff Hostetler (3):
read-cache: add strcmp_offset function
p0006-read-tree-checkout: perf test to time read-tree
read-cache: speed up add_index_entry during checkout
Makefile | 1 + cache.h | 1 + read-cache.c | 66 +++++++++++++++++++++- t/helper/.gitignore | 1 + t/helper/test-strcmp-offset.c | 22 ++++++++ t/perf/p0006-read-tree-checkout.sh | 69 +++++++++++++++++++++++ t/perf/repos/.gitignore | 1 + t/perf/repos/many-files.sh | 110 +++++++++++++++++++++++++++++++++++++ t/t0065-strcmp-offset.sh | 21 +++++++ 9 files changed, 290 insertions(+), 2 deletions(-) create mode 100644 t/helper/test-strcmp-offset.c create mode 100755 t/perf/p0006-read-tree-checkout.sh create mode 100644 t/perf/repos/.gitignore create mode 100755 t/perf/repos/many-files.sh create mode 100755 t/t0065-strcmp-offset.sh
Signature · 1 line
2.9.3
PATCH v9 1/3 read-cache: add strcmp_offset function
From: Jeff Hostetler <[email protected]>
Add strcmp_offset() function to also return the offset of the first change.
Add unit test and helper to verify.
--- Makefile | 1 + cache.h | 1 + read-cache.c | 20 ++++++++++++++++++++ t/helper/.gitignore | 1 + t/helper/test-strcmp-offset.c | 22 ++++++++++++++++++++++ t/t0065-strcmp-offset.sh | 21 +++++++++++++++++++++ 6 files changed, 66 insertions(+) create mode 100644 t/helper/test-strcmp-offset.c create mode 100755 t/t0065-strcmp-offset.sh
Makefile +1 -0
@@ -631,6 +631,7 @@ TEST_PROGRAMS_NEED_X += test-scrap-cache-tree TEST_PROGRAMS_NEED_X += test-sha1 TEST_PROGRAMS_NEED_X += test-sha1-array TEST_PROGRAMS_NEED_X += test-sigchain +TEST_PROGRAMS_NEED_X += test-strcmp-offset TEST_PROGRAMS_NEED_X += test-string-list TEST_PROGRAMS_NEED_X += test-submodule-config TEST_PROGRAMS_NEED_X += test-subprocess
cache.h +1 -0
@@ -574,6 +574,7 @@ extern int write_locked_index(struct index_state *, struct lock_file *lock, unsiextern int discard_index (struct index_state *);extern int unmerged_index (const struct index_state *);extern int verify_path (const char *path); +extern int strcmp_offset (const char *s1,const char *s2,size_t *first_change);extern int index_dir_exists (struct index_state *istate,const char *name,int namelen);extern void adjust_dirname_case (struct index_state *istate,char *name);extern struct cache_entry *index_file_exists (struct index_state *istate,const char *name,int namelen,int igncase);
read-cache.c +20 -0
@@ -887,6 +887,26 @@ static int has_file_name(struct index_state *istate,return retval; } + +/* +* Like strcmp(), but also return the offset of the first change. +* If strings are equal, return the length. +*/ +int strcmp_offset (const char *s1,const char *s2,size_t *first_change) +{ +size_t k; + +if (!first_change) +return strcmp (s1, s2); + +for (k =0 ; s1[k] == s2[k]; k++) +if (s1[k] =='\0' ) +break ; + + *first_change = k; +return (unsigned char )s1[k] - (unsigned char )s2[k]; +} +/* * Do we have another file with a pathname that is a proper * subset of the name we're trying to add?
t/helper/.gitignore +1 -0
@@ -25,6 +25,7 @@ /test-sha1 /test-sha1-array /test-sigchain +/test-strcmp-offset /test-string-list /test-submodule-config /test-subprocess
t/helper/test-strcmp-offset.c new file +22 -0
@@ -0,0 +1,22 @@ +#include "cache.h" + +int cmd_main (int argc,const char **argv) +{ +int result; +size_t offset; + +if (!argv[1 ] || !argv[2 ]) +die ("usage: %s <string1> <string2>" , argv[0 ]); + + result =strcmp_offset (argv[1 ], argv[2 ], &offset); + +/* +* Because differnt CRTs behave differently, only rely on signs +* of the result values. +*/ + result = (result <0 ?-1 : + result >0 ?1 : +0 ); +printf ("%d %" PRIuMAX"\n" , result, (uintmax_t )offset); +return 0 ; +}
t/t0065-strcmp-offset.sh new file +21 -0
@@ -0,0 +1,21 @@ +#!/bin/sh + +test_description='Test strcmp_offset functionality' + +. ./test-lib.sh + +while read s1 s2 expect +do + test_expect_success "strcmp_offset($s1, $s2)" ' + echo "$expect" >expect && + test-strcmp-offset "$s1" "$s2" >actual && + test_cmp expect actual + ' +done <<-EOF +abc abc 0 3 +abc def -1 0 +abc abz -1 2 +abc abcdef -1 3 +EOF + +test_done
Signature · 1 line
2.9.3
PATCH v9 2/3 p0006-read-tree-checkout: perf test to time read-tree
In reply to [email protected]From: Jeff Hostetler <[email protected]>
Created t/perf/repos/many-files.sh to generate large, but artificial repositories.
Created t/perf/p0006-read-tree-checkout.sh to measure performance on various read-tree, checkout, and update-index operations. This test can run using either artificial repos described above or normal repos.
--- t/perf/p0006-read-tree-checkout.sh | 69 +++++++++++++++++++++++ t/perf/repos/.gitignore | 1 + t/perf/repos/many-files.sh | 110 +++++++++++++++++++++++++++++++++++++ 3 files changed, 180 insertions(+) create mode 100755 t/perf/p0006-read-tree-checkout.sh create mode 100644 t/perf/repos/.gitignore create mode 100755 t/perf/repos/many-files.sh
t/perf/p0006-read-tree-checkout.sh new file +69 -0
@@ -0,0 +1,69 @@ +#!/bin/sh +## +## This test measures the performance of various read-tree +## and checkout operations. It is primarily interested in +## the algorithmic costs of index operations and recursive +## tree traversal -- and NOT disk I/O on thousands of files. + +test_description="Tests performance of read-tree" + +. ./perf-lib.sh + +test_perf_default_repo + +## If the test repo was generated by ./repos/many-files.sh +## then we know something about the data shape and branches, +## so we can isolate testing to the ballast-related commits +## and setup sparse-checkout so we don't have to populate +## the ballast files and directories. +## +## Otherwise, we make some general assumptions about the +## repo and consider the entire history of the current +## branch to be the ballast. + +git branch | grep p0006-ballast >/dev/null 2>&1 +synthetic=$? +if test "$synthetic" = 0 +then + echo Assuming synthetic repo from many-files.sh + git branch br_base master + git branch br_ballast p0006-ballast^ + git branch br_ballast_alias p0006-ballast^ + git branch br_ballast_plus_1 p0006-ballast + echo '/*' >.git/info/sparse-checkout + echo '!ballast/*' >>.git/info/sparse-checkout + git config --local core.sparsecheckout 1 +else + echo Assuming non-synthetic repo... + git branch br_base $(git rev-list HEAD | tail -n 1) + git branch br_ballast HEAD^ || error "no ancestor commit from current head" + git branch br_ballast_alias HEAD^ + git branch br_ballast_plus_1 HEAD +fi + +test_expect_success 'setup' ' + git checkout -q br_ballast +' + +nr_files=$(git ls-files | wc -l) + +test_perf "read-tree br_base br_ballast ($nr_files)" ' + git read-tree -m br_base br_ballast -n +' + +test_perf "switch between br_base br_ballast ($nr_files)" ' + git checkout -q br_base && + git checkout -q br_ballast +' + +test_perf "switch between br_ballast br_ballast_plus_1 ($nr_files)" ' + git checkout -q br_ballast_plus_1 && + git checkout -q br_ballast +' + +test_perf "switch between aliases ($nr_files)" ' + git checkout -q br_ballast_alias && + git checkout -q br_ballast +' + +test_done
t/perf/repos/.gitignore new file +1 -0
@@ -0,0 +1 @@ +gen-*/
t/perf/repos/many-files.sh new file +110 -0
110 lines not on this page · raw message
Signature · 1 line
2.9.3
PATCH v9 3/3 read-cache: speed up add_index_entry during checkout
In reply to [email protected]From: Jeff Hostetler <[email protected]>
Teach add_index_entry_with_check() and has_dir_name() to see if the path of the new item is greater than the last path in the index array before attempting to search for it.
During checkout, merge_working_tree() populates the new index in sorted order, so this change will save at least 2 binary lookups per file. This preserves the original behavior but simply checks the last element before starting the search.
This helps performance on very large repositories.
This can be seen using p0006-read-tree-checkout.sh and the artificial repository created by t/perf/repos/many-files.sh
with parameters (5, 10, 9). (1M files in index.)
Test HEAD^ HEAD
----------------------------------------------------------------------------------------------------------
0006.2: read-tree br_base br_ballast (1000001) 4.15(2.72+1.41) 3.21(1.97+1.22) -22.7% 0006.3: switch between br_base br_ballast (1000001) 8.11(5.57+2.28) 6.77(4.36+2.14) -16.5% 0006.4: switch between br_ballast br_ballast_plus_1 (1000001) 13.52(8.68+4.35) 11.80(7.38+3.96) -12.7% 0006.5: switch between aliases (1000001) 13.59(8.75+4.43) 11.85(7.49+3.87) -12.8%
On linux.git, results are:
Test HEAD^ HEAD
------------------------------------------------------------------------------------------------------
0006.2: read-tree br_base br_ballast (57994) 0.24(0.22+0.01) 0.20(0.17+0.02) -16.7% 0006.3: switch between br_base br_ballast (57994) 9.91(5.82+2.79) 10.26(5.92+2.77) +3.5% 0006.4: switch between br_ballast br_ballast_plus_1 (57994) 0.59(0.44+0.16) 0.50(0.34+0.18) -15.3% 0006.5: switch between aliases (57994) 0.62(0.48+0.16) 0.50(0.35+0.16) -19.4%
--- read-cache.c | 46 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-)
read-cache.c +44 -2
@@ -918,9 +918,24 @@ static int has_dir_name(struct index_state *istate,int stage =ce_stage (ce);const char *name = ce->name;const char *slash = name +ce_namelen (ce); +size_t len_eq_last; +int cmp_last =0 ; + +if (istate->cache_nr >0 ) { +/* +* Compare the entry's full path with the last path in the index. +* If it sorts AFTER the last entry in the index and they have no +* common prefix, then there cannot be any F/D name conflicts. +*/ + cmp_last =strcmp_offset (name, + istate->cache[istate->cache_nr-1 ]->name, + &len_eq_last); +if (cmp_last >0 && len_eq_last ==0 ) +return retval; + }for (;;) { -int len; +size_t len;for (;;) {if (*--slash =='/' )
@@ -930,6 +945,24 @@ static int has_dir_name(struct index_state *istate, } len = slash - name; +if (cmp_last >0 ) { +/* +* If this part of the directory prefix (including the trailing +* slash) already appears in the path of the last entry in the +* index, then we cannot also have a file with this prefix (or +* any parent directory prefix). +*/ +if (len+1 <= len_eq_last) +return retval; +/* +* If this part of the directory prefix (excluding the trailing +* slash) is longer than the known equal portions, then this part +* of the prefix cannot collide with a file. Go on to the parent. +*/ +if (len > len_eq_last) +continue ; + } + pos =index_name_stage_pos (istate, name, len, stage);if (pos >=0 ) {/*
@@ -1021,7 +1054,16 @@ static int add_index_entry_with_check(struct index_state *istate, struct cache_eif (!(option &ADD_CACHE_KEEP_CACHE_TREE ))cache_tree_invalidate_path (istate, ce->name); - pos =index_name_stage_pos (istate, ce->name,ce_namelen (ce),ce_stage (ce)); + +/* +* If this entry's path sorts after the last entry in the index, +* we can avoid searching for it. +*/ +if (istate->cache_nr >0 && +strcmp (ce->name, istate->cache[istate->cache_nr -1 ]->name) >0 ) + pos = -istate->cache_nr -1 ; +else + pos =index_name_stage_pos (istate, ce->name,ce_namelen (ce),ce_stage (ce));/* existing match? Just replace it. */ if (pos >=0 ) {
Signature · 1 line
2.9.3
Re: [PATCH v9 3/3] read-cache: speed up add_index_entry during checkout
Quoted 10 lines · [email protected] writes:
From: Jeff Hostetler <[email protected]>
Teach
add_index_entry_with_check()andhas_dir_name()to see if the path of the new item is greater than the last path in the index array before attempting to search for it.During checkout,
merge_working_tree()populates the new index in sorted order, so this change will save at least 2 binary lookups per file.
Smart and simple.
Quoted 9 lines
read-cache.c +1
-0@@ -918,9 +918,24 @@ static int has_dir_name(struct index_state *istate,int stage =ce_stage (ce);const char *name = ce->name;const char *slash = name +ce_namelen (ce); +size_t len_eq_last;
read-cache.c static int has_dir_name(struct index_state *istate, · line 931
Quoted 10 lines
read-cache.c +10
-0+int cmp_last =0 ; + +if (istate->cache_nr >0 ) { +/* +* Compare the entry's full path with the last path in the index. +* If it sorts AFTER the last entry in the index and they have no +* common prefix, then there cannot be any F/D name conflicts. +*/ + cmp_last = strcmp_offset(name, + istate->cache[istate->cache_nr-1]->name,
Style? "istate->cache[istate->cache_nr - 1]->name"
read-cache.c static int has_dir_name(struct index_state *istate, · line 935
Quoted 4 lines
read-cache.c +4
-0+ &len_eq_last); +if (cmp_last >0 && len_eq_last ==0 ) +return retval; + }
Let me follow the logic aloud. Say the last entry in the cache is "x/y". If we came here with ce->name == "x", we need to worry about nuking the existing entry "x/y". But if we have "zoo", that cannot possibly overlap and we can safely return 0.
That sounds correct, except that it might be playing overly safe. If we came here with "xx", we still are safe, but len_eq_last would be non-zero. Probably it is not worth the extra complexity to catch it here (rather than letting the loop below to handle it).
read-cache.c static int has_dir_name(struct index_state *istate, · line 946
Quoted 9 lines
read-cache.c +1
-1for (;;) { -int len; +size_t len;for (;;) {if (*--slash =='/' )@@ -930,6 +945,24 @@ static int has_dir_name(struct index_state *istate, } len = slash - name;
Mental note: cmp_last may be 0, >0 or <0 at this point in the very first iteration of the loop. It is not updated in the loop. The variable len_eq_last are used to carry the information about the last entry we learned at the beginning of this function---the new special case happens only when the path we are adding sorts later than the last existing entry (i.e. cmp_last > 0).
read-cache.c static int has_dir_name(struct index_state *istate, · line 955
Quoted 8 lines
read-cache.c +8
-0+if (cmp_last >0 ) { +/* +* If this part of the directory prefix (including the trailing +* slash) already appears in the path of the last entry in the +* index, then we cannot also have a file with this prefix (or +* any parent directory prefix). +*/ +if (len+1 <= len_eq_last)
Style? "len + 1".
read-cache.c static int has_dir_name(struct index_state *istate, · line 963
Quoted 8 lines
read-cache.c +8
-0+return retval; +/* +* If this part of the directory prefix (excluding the trailing +* slash) is longer than the known equal portions, then this part +* of the prefix cannot collide with a file. Go on to the parent. +*/ +if (len > len_eq_last) +continue ;
Hmph, is the reasoning used in the two conditionals above sound? Does this work correctly even when the last existing entry in the cache is marked with CE_REMOVE?
Quoted 10 lines
read-cache.c +2
-1+ } + pos =index_name_stage_pos (istate, name, len, stage);if (pos >=0 ) {/* @@ -1021,7 +1054,16 @@ static int add_index_entry_with_check(struct index_state *istate, struct cache_eif (!(option &ADD_CACHE_KEEP_CACHE_TREE ))cache_tree_invalidate_path (istate, ce->name); - pos =index_name_stage_pos (istate, ce->name,ce_namelen (ce),ce_stage (ce));
read-cache.c static int add_index_entry_with_check(struct index_state *istate, struct cache_e · line 1066
Quoted 10 lines
read-cache.c +10
-0+ +/* +* If this entry's path sorts after the last entry in the index, +* we can avoid searching for it. +*/ +if (istate->cache_nr >0 && +strcmp (ce->name, istate->cache[istate->cache_nr -1 ]->name) >0 ) + pos = -istate->cache_nr -1 ; +else + pos =index_name_stage_pos (istate, ce->name,ce_namelen (ce),ce_stage (ce));
This one (unlike the change to has_dir_name()) is trivially and obviously correct.
Quoted 2 lines
read-cache.c +0
-0/* existing match? Just replace it. */ if (pos >=0 ) {
Re: [PATCH v9 3/3] read-cache: speed up add_index_entry during checkout
Quoted 80 lines · Junio C Hamano
Quoted 10 lines · [email protected] writes:
From: Jeff Hostetler <[email protected]>
Teach
add_index_entry_with_check()andhas_dir_name()to see if the path of the new item is greater than the last path in the index array before attempting to search for it.During checkout,
merge_working_tree()populates the new index in sorted order, so this change will save at least 2 binary lookups per file.Smart and simple.
Quoted 9 lines
read-cache.c +1
-0@@ -918,9 +918,24 @@ static int has_dir_name(struct index_state *istate,int stage =ce_stage (ce);const char *name = ce->name;const char *slash = name +ce_namelen (ce); +size_t len_eq_last;read-cache.c static int has_dir_name(struct index_state *istate, · line 931
Quoted 10 lines
read-cache.c +10
-0+int cmp_last =0 ; + +if (istate->cache_nr >0 ) { +/* +* Compare the entry's full path with the last path in the index. +* If it sorts AFTER the last entry in the index and they have no +* common prefix, then there cannot be any F/D name conflicts. +*/ + cmp_last = strcmp_offset(name, + istate->cache[istate->cache_nr-1]->name,Style? "istate->cache[istate->cache_nr - 1]->name"
read-cache.c static int has_dir_name(struct index_state *istate, · line 935
Quoted 4 lines
read-cache.c +4
-0+ &len_eq_last); +if (cmp_last >0 && len_eq_last ==0 ) +return retval; + }Let me follow the logic aloud. Say the last entry in the cache is "x/y". If we came here with
ce->name== "x", we need to worry about nuking the existing entry "x/y". But if we have "zoo", that cannot possibly overlap and we can safely return 0.That sounds correct, except that it might be playing overly safe. If we came here with "xx", we still are safe, but
len_eq_lastwould be non-zero. Probably it is not worth the extra complexity to catch it here (rather than letting the loop below to handle it).read-cache.c static int has_dir_name(struct index_state *istate, · line 946
Quoted 9 lines
read-cache.c +1
-1for (;;) { -int len; +size_t len;for (;;) {if (*--slash =='/' )@@ -930,6 +945,24 @@ static int has_dir_name(struct index_state *istate, } len = slash - name;Mental note:
cmp_lastmay be 0, >0 or <0 at this point in the very first iteration of the loop. It is not updated in the loop. The variablelen_eq_lastare used to carry the information about the last entry we learned at the beginning of this function---the new special case happens only when the path we are adding sorts later than the last existing entry (i.e.cmp_last> 0).read-cache.c static int has_dir_name(struct index_state *istate, · line 955
Quoted 8 lines
read-cache.c +8
-0+if (cmp_last >0 ) { +/* +* If this part of the directory prefix (including the trailing +* slash) already appears in the path of the last entry in the +* index, then we cannot also have a file with this prefix (or +* any parent directory prefix). +*/ +if (len+1 <= len_eq_last)Style? "len + 1".
Quoted 12 lines
read-cache.c static int has_dir_name(struct index_state *istate, · line 963
Quoted 8 lines
read-cache.c +8
-0+return retval; +/* +* If this part of the directory prefix (excluding the trailing +* slash) is longer than the known equal portions, then this part +* of the prefix cannot collide with a file. Go on to the parent. +*/ +if (len > len_eq_last) +continue ;Hmph, is the reasoning used in the two conditionals above sound? Does this work correctly even when the last existing entry in the cache is marked with
CE_REMOVE?
I'll double check my math here. I also want to think about the "too conservative" comment above.
WRT if the last entry is CE_REMOVE, I think we are still OK because we are asking if the given ce is strictly greater than the last entry so that we can append it rather than searching for a collision or insertion point.
Thanks,
Jeff
Quoted 3 lines · On Tue, Apr 11, 2017 at 07:16:59PM +0000, [email protected] wrote:
I think this version has addressed everything raise so far, so I think I'm ready to let this one rest. Thanks for all the help and feedback.
Yeah, this addresses my nitpicks with the perf stuff. Thanks for sticking with it.
-Peff