read-cache: speed up add_index_entry
v8 cover read-cache: speed up add_index_entry
From: Jeff Hostetler <[email protected]>
Version 8 addresses the following:
() Refactors strcmp_offset() test helper to just compare the
values and return results to stdout.
() Updates t0065 to have the actual test logic.
() Splits the synthetic test repo generation out of the perf
test into a stand-alone script.
() Simplifies the p0006 (was p0004) perf test to use an
existing repository (whether artificial or real).
() Adds additional tests for add/delete (which should be
driven by the arg-list rather than an iteration on the index.
() Updated the read-cache commit message with new perf data.
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 | 90 ++++++++++++++++++++++++++++ t/perf/repos/.gitignore | 1 + t/perf/repos/many-files.sh | 118 +++++++++++++++++++++++++++++++++++++ t/t0065-strcmp-offset.sh | 21 +++++++ 9 files changed, 319 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 v8 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 v8 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.
--- t/perf/p0006-read-tree-checkout.sh | 90 ++++++++++++++++++++++++++++ t/perf/repos/.gitignore | 1 + t/perf/repos/many-files.sh | 118 +++++++++++++++++++++++++++++++++++++ 3 files changed, 209 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 +90 -0
@@ -0,0 +1,90 @@ +#!/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. +## Therefore, it uses sparse-checkout to avoid populating +## the ballast files. +## +## It expects the test repo to have certain characteristics. +## Branches: +## () master := an arbitrary commit. +## () ballast := an arbitrary commit with a large number +## of changes relative to "master". +## () ballast-alias := a branch pointing to the same commit +## as "ballast". +## () ballast-1 := a commit with a 1 file difference from +## "ballast". +## +## Ballast files in the repository should not appear in +## the sparse-checkout (if present). +## +## See "t/perf/repos/many-files.sh" to generate some +## synthetic data. +## + +test_description="Tests performance of read-tree" + +. ./perf-lib.sh + +test_perf_default_repo + +test_expect_success 'setup' ' + echo "sparse/*" >>.git/info/sparse-checkout && + git config --local core.sparsecheckout 1 && + git checkout ballast +' + +nr_files=$(git ls-files | wc -l) + +test_perf "read-tree master ballast ($nr_files)" ' + git read-tree -m master ballast -n +' + +## Alternate between a commit with and without the ballast. +test_perf "switch between master ballast ($nr_files)" ' + git checkout master && + git checkout ballast +' + +## Alternate between 2 commits with the ballast and 1 change. +test_perf "switch between ballast ballast-1 ($nr_files)" ' + git checkout ballast-1 && + git checkout ballast +' + +## Alternate between 2 aliases for the same commit. +test_perf "switch between aliases ballast ballast-alias ($nr_files)" ' + git checkout ballast-alias && + git checkout ballast +' + +export nr_random=200000 + +## Add random items in sorted order. These will effectively +## get appended because "zzzz/" sorts after anything already +## present in index. Then delete them in reverse order, so +## that the last is removed each time. +test_perf "add forward sorted items ($nr_files) ($nr_random)" ' + awk "BEGIN { for (f = 0; f <= $nr_random; f++) printf \"r_%06d\n\", f }" | + sed "s|^|100644 $EMPTY_BLOB zzzz/|" | + git update-index --index-info && + awk "BEGIN { for (f = $nr_random; f >= 0; f--) printf \"r_%06d\n\", f }" | + sed "s|^|000000 $EMPTY_BLOB zzzz/|" | + git update-index --index-info +' + +## Add random items in reverse order. These will be inserted +## NEAR the end of the index, but not at the end. Then delete +## them in the opposite order, so that we remove from the middle. +test_perf "add reverse sorted items ($nr_files) ($nr_random)" ' + awk "BEGIN { for (f = $nr_random; f >= 0; f--) printf \"r_%06d\n\", f }" | + sed "s|^|100644 $EMPTY_BLOB zzzz/|" | + git update-index --index-info && + awk "BEGIN { for (f = 0; f <= $nr_random; f++) printf \"r_%06d\n\", f }" | + sed "s|^|000000 $EMPTY_BLOB zzzz/|" | + git update-index --index-info +' + +test_done
t/perf/repos/.gitignore new file +1 -0
@@ -0,0 +1 @@ +gen-*/
t/perf/repos/many-files.sh new file +118 -0
118 lines not on this page · raw message
Signature · 1 line
2.9.3
Re: [PATCH v8 2/3] p0006-read-tree-checkout: perf test to time read-tree
Quoted 4 lines · On Mon, Apr 10, 2017 at 09:14:02PM +0000, [email protected] wrote:
From: Jeff Hostetler <[email protected]>
Created
t/perf/repos/many-files.shto generate large, but artificial repositories.
I think this is a good direction. In the long run we might want some kind of magic to pull from the "library" of repos when running perf tests, but it's not a big deal to run the script manually and point GIT_PERF_REPO at the result.
As a bonus, this should be faster when running perf tests, since we can reuse the built repo when testing each version of Git.
Quoted 6 lines
t/perf/p0006-read-tree-checkout.sh new file +6
-0+## 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. +## Therefore, it uses sparse-checkout to avoid populating +## the ballast files.
t/perf/p0006-read-tree-checkout.sh line 18
Quoted 10 lines
t/perf/p0006-read-tree-checkout.sh new file +10
-0+## +## It expects the test repo to have certain characteristics. +## Branches: +## () master := an arbitrary commit. +## () ballast := an arbitrary commit with a large number +## of changes relative to "master". +## () ballast-alias := a branch pointing to the same commit +## as "ballast". +## () ballast-1 := a commit with a 1 file difference from +## "ballast".
I'm OK with leaving these requirements on the repo in the name of simplicity, though it does make it harder to perf-test against a regular repo.
I wonder if we could make reasonable guesses, like:
master => HEAD ballast => $(git rev-list HEAD | tail -n 1) ballast-alias => git branch $ballast ballast-1 => HEAD^
That would approximate your conditions in a real-world repository, and it should be easy to make your synthetic one fit the bill exactly.
I don't know if you'd want to turn on sparse checkout manually or not when testing a real-world repo.
-Peff
PATCH v8 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.)
1..7
Test HEAD^ HEAD
------------------------------------------------------------------------------------------------------------
0006.2: read-tree master ballast (1000001) 4.01(2.71+1.28) 3.24(1.84+1.38) -19.2% 0006.3: switch between master ballast (1000001) 8.23(5.60+2.45) 6.73(4.20+2.35) -18.2% 0006.4: switch between ballast ballast-1 (1000001) 13.36(8.60+4.35) 11.84(7.28+4.08) -11.4% 0006.5: switch between aliases ballast ballast-alias (1000001) 13.43(8.68+4.27) 12.09(7.28+4.37) -10.0% 0006.6: add forward sorted items (1000001) (200000) 2.42(1.76+0.42) 2.40(1.66+0.37) -0.8% 0006.7: add reverse sorted items (1000001) (200000) 18.59(17.96+0.33) 18.70(18.07+0.42) +0.6%
--- 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