From: Jeff King <hidden> Date: 2016-06-15 22:43:45
This is my first stab at faster rename handling based on Andy's code.
The patches are on top of next (to get Linus' recent work on exact
renames). Most of the interesting stuff is in 2/3.
1/3: extension of hash interface
2/3: similarity detection code
3/3: integrate similarity detection into diffcore-rename
The implementation is pretty basic, so I think there is room for
code optimization (50% of the time is spent in hash lookups, so we might
be able to micro-optimize that) as well as algorithmic improvements (like the
sampling Andy mentioned).
With these patches, I can get my monster binary diff down from about 2
minutes to 17 seconds. And comparing all of linux-2.4 to all of
linux-2.6 (similar to Andy's previous demo) takes about 10 seconds.
There are a few downsides:
- the current implementation tends to give lower similarity values
compared to the old code (see discussion in 2/3), but this should be
tweakable
- on large datasets, it's more memory hungry than the old code because
the hash grows very large. This can be helped by bumping up the
binary chunk size (actually, the 17 seconds quoted above is using
256-byte chunks rather than 64-byte -- with 64-byte chunks, it's
more like 24 seconds) as well as sampling.
- no improvement on smaller datasets. Running "git-whatchanged -M
--raw -l0" on the linux-2.6 repo takes about the same time with the
old and new code (presumably the algorithmic savings of the new code
are lost in a higher constant factor, so when n is small, it is a
wash).
-Peff
From: Jeff King <hidden> Date: 2016-06-15 22:43:45
The recent hash table code has two limitations in its
calling conventions that are addressed here:
1. Insertion either inserts a value, returning NULL, or
returns a pointer to the previously inserted value.
This is fine if you are making a linked list of the
colliding values, but is awkward if your goal is to:
a. modify the value if it already exists
b. otherwise, allocate and insert the value
With the old convention, you must either allocate the
structure (and throw it away in case a), or perform two
lookups (one to see if the entry exists, then another
to perform the insertion).
Instead, insertion no longer inserts any value; it
simply returns a pointer to where you _can_ insert a
value (which will be non-NULL if a value already
existed).
2. for_each_hash now allows a void 'data' pointer to be
passed to the callback function along with each hash
entry.
Signed-off-by: Jeff King <redacted>
---
The insertion feels kind of hack-ish. Suggestions are welcome.
diffcore-rename.c | 14 +++++---------
hash.c | 18 +++++++++---------
hash.h | 5 +++--
3 files changed, 17 insertions(+), 20 deletions(-)
@@ -343,13 +343,9 @@ static void insert_file_table(struct hash_table *table, int src_dst, int index,entry->next=NULL;hash=hash_filespec(filespec);-pos=insert_hash(hash,entry,table);--/* We already had an entry there? */-if(pos){-entry->next=*pos;-*pos=entry;-}+pos=insert_hash(hash,table);+entry->next=*pos;+*pos=entry;}/*
@@ -372,7 +368,7 @@ static int find_exact_renames(void)insert_file_table(&file_table,1,i,rename_dst[i].two);/* Find the renames */-i=for_each_hash(&file_table,find_same_files);+i=for_each_hash(&file_table,find_same_files,NULL);/* .. and free the hash data structure */free_hash(&file_table);
From: Jeff King <hidden> Date: 2016-06-15 22:43:45
This library attempts to find similarities among items
efficiently. It treats items as opaque pointers; callers are
responsible for providing an item along with its contents.
The algorithm is roughly:
1. for each item, create a set of fingerprints for each
chunk of the item (where each chunk is either delimited
by a newline or is 64 characters, whichever is smaller
-- this is the same fingerprint code from
diffcore-delta.c). A hash stores a mapping of
fingerprints to items, with each fingerprint having at
most one 'source' item and one 'dest' item.
2. for each fingerprint with a source and dest item,
find the entry with key (source, dest) in a hash table
and increment its value by the value of the fingerprint
3. for each (source, dest) pair that had non-zero
similarity, report the pair to the caller
The program test-similarity is a simple demonstration of the
code. It takes two list of files on stdin, with each file
separated by newlines and the two lists separated by a blank
line. It prints the similarity score of each non-zero pair
on stdout.
There are a few "interesting" design decisions, which should
probably be tweaked:
- we store only one source and dest for each fingerprint
item. We need to bound this list so that we don't get
O(n^2) behavior for common fingerprints. This means that
some files won't get "credit" for common fingerprints,
which is probably OK, since those fingerprints are
probably uninteresting. We could bound at some small
number greater than one; one was chosen for speed and
simplicity of implementation. We also don't store any
overflow bit, so commonly used fingerprints will get
assigned to whatever file is found with them last.
- the similarity engine hands back absolute,
non-normalized scores. That means that bigger items will
have bigger scores, and the caller is responsible for
normalizing. This also means that the engine cannot
adjust normalization to account for chunks which are
thrown out by the bounding mentioned above (i.e., if a
file is 80 bytes, 64 of which are ignored as "common",
then it can at most have 20% similarity (16/80) with
another file. This means our normalized rename values
(i.e., percentage similarity) will be lower than other
algorithms.
Signed-off-by: Jeff King <redacted>
---
.gitignore | 1 +
Makefile | 4 +-
similarity.c | 152 +++++++++++++++++++++++++++++++++++++++++++++++++++++
similarity.h | 24 ++++++++
test-similarity.c | 54 +++++++++++++++++++
5 files changed, 233 insertions(+), 2 deletions(-)
create mode 100644 similarity.c
create mode 100644 similarity.h
create mode 100644 test-similarity.c
@@ -0,0 +1,152 @@+#include"cache.h"+#include"similarity.h"++structfingerprint_entry{+void*src;+void*dst;+unsignedweight;+};++structscore_entry{+void*src;+void*dst;+unsignedscore;+structscore_entry*next;+};++voidsimilarity_init(structsimilarity*s)+{+init_hash(&s->fingerprints);+init_hash(&s->scores);+}++staticintfree_one_fingerprint(void*ve,void*data)+{+structfingerprint_entry*e=ve;+free(e);+return0;+}++staticintfree_one_score(void*ve,void*data)+{+structscore_entry*e=ve;+while(e){+structscore_entry*next=e->next;+free(e);+e=next;+}+return0;+}++voidsimilarity_free(structsimilarity*s)+{+for_each_hash(&s->fingerprints,free_one_fingerprint,NULL);+free_hash(&s->fingerprints);+for_each_hash(&s->scores,free_one_score,NULL);+free_hash(&s->scores);+}++staticvoidadd_fingerprint(structhash_table*h,unsignedintfp,+inttype,void*data,unsignedweight)+{+void**pos;+structfingerprint_entry*e;++pos=insert_hash(fp,h);+if(!*pos){+e=xmalloc(sizeof(*e));+e->weight=weight;+e->src=e->dst=NULL;+*pos=e;+}+else+e=*pos;++if(type==SIMILARITY_SOURCE)+e->src=data;+else+e->dst=data;+}++voidsimilarity_add(structsimilarity*sim,inttype,void*data,+constchar*buf,unsignedlongsz,intis_text)+{+intn;+unsignedintaccum1,accum2,hashval;++n=0;+accum1=accum2=0;+while(sz){+unsignedintc=*buf++;+unsignedintold_1=accum1;+sz--;++/* Ignore CR in CRLF sequence if text */+if(!is_text&&c=='\r'&&sz&&*buf=='\n')+continue;++accum1=(accum1<<7)^(accum2>>25);+accum2=(accum2<<7)^(old_1>>25);+accum1+=c;+if(++n<64&&c!='\n')+continue;+hashval=accum1+accum2*0x61;+add_fingerprint(&sim->fingerprints,hashval,type,data,n);+n=0;+accum1=accum2=0;+}+}++staticunsignedhash_void_pair(void*a,void*b)+{+return(unsigned)a+(unsigned)b;+}++staticintscore_one_entry(void*vfp,void*vsim)+{+structfingerprint_entry*fp=vfp;+structsimilarity*sim=vsim;+structscore_entry*score;+void**pos;++if(!fp->src||!fp->dst)+return0;++pos=insert_hash(hash_void_pair(fp->src,fp->dst),&sim->scores);+for(score=*pos;score;score=score->next){+if(score->src==fp->src&&score->dst==fp->dst){+score->score+=fp->weight;+return0;+}+}++score=xmalloc(sizeof(*score));+score->src=fp->src;+score->dst=fp->dst;+score->score=fp->weight;+score->next=*pos;+*pos=score;++return0;+}++voidsimilarity_score(structsimilarity*s)+{+for_each_hash(&s->fingerprints,score_one_entry,s);+}++staticintreport_one_score(void*ve,void*vdata)+{+structscore_entry*e;+void(*fn)(void*,void*,unsigned)=vdata;++for(e=ve;e;e=e->next)+fn(e->src,e->dst,e->score);+return1;+}++voidsimilarity_report(structsimilarity*s,+void(*fn)(void*,void*,unsigned))+{+for_each_hash(&s->scores,report_one_score,fn);+}
From: Jeff King <hidden> Date: 2016-06-15 22:43:45
This changes diffcore-rename to use the engine in
similarity.c rather than doing an O(m*n) loop around
diffcore_count_changes.
Signed-off-by: Jeff King <redacted>
---
diffcore-rename.c | 215 +++++++++++++++++++----------------------------------
1 files changed, 76 insertions(+), 139 deletions(-)
@@ -5,12 +5,21 @@#include"diff.h"#include"diffcore.h"#include"hash.h"+#include"similarity.h"-/* Table of rename/copy destinations */+/* Table of rename/copy src files */+staticstructdiff_rename_src{+structdiff_filespec*one;+unsignedshortscore;/* to remember the break score */+}*rename_src;+staticintrename_src_nr,rename_src_alloc;+/* Table of rename/copy destinations */staticstructdiff_rename_dst{structdiff_filespec*two;structdiff_filepair*pair;+structdiff_rename_src*best_match;+unsignedscore;}*rename_dst;staticintrename_dst_nr,rename_dst_alloc;
@@ -49,16 +58,11 @@ static struct diff_rename_dst *locate_rename_dst(struct diff_filespec *two,rename_dst[first].two=alloc_filespec(two->path);fill_filespec(rename_dst[first].two,two->sha1,two->mode);rename_dst[first].pair=NULL;+rename_dst[first].best_match=NULL;+rename_dst[first].score=0;return&(rename_dst[first]);}-/* Table of rename/copy src files */-staticstructdiff_rename_src{-structdiff_filespec*one;-unsignedshortscore;/* to remember the break score */-}*rename_src;-staticintrename_src_nr,rename_src_alloc;-staticstructdiff_rename_src*register_rename_src(structdiff_filespec*one,unsignedshortscore){
@@ -109,88 +113,6 @@ static int basename_same(struct diff_filespec *src, struct diff_filespec *dst)(!dst_len||dst->path[dst_len-1]=='/');}-structdiff_score{-intsrc;/* index in rename_src */-intdst;/* index in rename_dst */-intscore;-intname_score;-};--staticintestimate_similarity(structdiff_filespec*src,-structdiff_filespec*dst,-intminimum_score)-{-/* src points at a file that existed in the original tree (or-*optionallyafileinthedestinationtree)anddstpoints-*atanewlycreatedfile.Theymaybequitesimilar,inwhich-*casewewanttosaysrcisrenamedtodstorsrciscopiedinto-*dst,andthensomeedithasbeenappliedtodst.-*-*Comparethemandreturnhowsimilartheyare,representing-*thescoreasanintegerbetween0andMAX_SCORE.-*-*Whenthereisanexactmatch,itisconsideredabetter-*matchthananythingelse;thedestinationdoesnoteven-*callintothisfunctioninthatcase.-*/-unsignedlongmax_size,delta_size,base_size,src_copied,literal_added;-unsignedlongdelta_limit;-intscore;--/* We deal only with regular files. Symlink renames are handled-*onlywhentheyareexactmatches---inotherwords,noedits-*afterrenaming.-*/-if(!S_ISREG(src->mode)||!S_ISREG(dst->mode))-return0;--/*-*Needtocheckthatsourceanddestinationsizesare-*filledinbeforecomparingthem.-*-*Ifwealreadyhave"cnt_data"filledin,weknowit's-*allgood(avoidcheckingthesizeforzero,asthat-*isapossiblesize-wereallyshouldhaveaflagto-*saywhetherthesizeisvalidornot!)-*/-if(!src->cnt_data&&diff_populate_filespec(src,0))-return0;-if(!dst->cnt_data&&diff_populate_filespec(dst,0))-return0;--max_size=((src->size>dst->size)?src->size:dst->size);-base_size=((src->size<dst->size)?src->size:dst->size);-delta_size=max_size-base_size;--/* We would not consider edits that change the file size so-*drastically.delta_sizemustbesmallerthan-*(MAX_SCORE-minimum_score)/MAX_SCORE*min(src->size,dst->size).-*-*Notethatbase_size==0caseishandledherealready-*andthefinalscorecomputationbelowwouldnothavea-*divide-by-zeroissue.-*/-if(base_size*(MAX_SCORE-minimum_score)<delta_size*MAX_SCORE)-return0;--delta_limit=(unsignedlong)-(base_size*(MAX_SCORE-minimum_score)/MAX_SCORE);-if(diffcore_count_changes(src,dst,-&src->cnt_data,&dst->cnt_data,-delta_limit,-&src_copied,&literal_added))-return0;--/* How similar are they?-*whatpercentageofmaterialindstarefromsource?-*/-if(!dst->size)-score=0;/* should not happen */-else-score=(int)(src_copied*MAX_SCORE/max_size);-returnscore;-}-staticvoidrecord_rename_pair(intdst_index,intsrc_index,intscore){structdiff_filespec*src,*dst;
@@ -215,20 +137,6 @@ static void record_rename_pair(int dst_index, int src_index, int score)rename_dst[dst_index].pair=dp;}-/*-*Wesorttherenamesimilaritymatrixwiththescore,indescending-*order(themostsimilarfirst).-*/-staticintscore_compare(constvoid*a_,constvoid*b_)-{-conststructdiff_score*a=a_,*b=b_;--if(a->score==b->score)-returnb->name_score-a->name_score;--returnb->score-a->score;-}-structfile_similarity{intsrc_dst,index;structdiff_filespec*filespec;
@@ -376,6 +284,67 @@ static int find_exact_renames(void)returni;}+staticvoidrecord_similarity(void*vsrc,void*vdst,unsignedscore)+{+structdiff_rename_src*src=vsrc;+structdiff_rename_dst*dst=vdst;+unsignedmax_size=(src->one->size>dst->two->size)?+src->one->size:dst->two->size;++score=(dst->two->size!=0)?(score*MAX_SCORE/max_size):0;++/* Is there a match already that is better than we are? */+if(dst->best_match){+if(score<dst->score)+return;+if(score==dst->score&&!basename_same(src->one,dst->two))+return;+}++dst->best_match=src;+dst->score=score;+}++staticvoidfind_approximate_renames(intminimum_score)+{+structsimilaritysim;+inti;++similarity_init(&sim);++for(i=0;i<rename_src_nr;i++){+structdiff_rename_src*s=&rename_src[i];+diff_populate_filespec(s->one,0);+similarity_add(&sim,SIMILARITY_SOURCE,s,+s->one->data,s->one->size,+diff_filespec_is_binary(s->one));+diff_free_filespec_data(s->one);+}++for(i=0;i<rename_dst_nr;i++){+structdiff_rename_dst*d=&rename_dst[i];+if(d->pair)+continue;+diff_populate_filespec(d->two,0);+similarity_add(&sim,SIMILARITY_DEST,d,+d->two->data,d->two->size,+diff_filespec_is_binary(d->two));+diff_free_filespec_data(d->two);+}++similarity_score(&sim);+similarity_report(&sim,record_similarity);++for(i=0;i<rename_dst_nr;i++){+structdiff_rename_dst*d=&rename_dst[i];+if(d->pair)+continue;+if(d->score<minimum_score)+continue;+record_rename_pair(i,d->best_match-rename_src,d->score);+}+}+voiddiffcore_rename(structdiff_options*options){intdetect_rename=options->detect_rename;
@@ -462,38 +430,7 @@ void diffcore_rename(struct diff_options *options)if(num_create*num_src>rename_limit*rename_limit)gotocleanup;-mx=xmalloc(sizeof(*mx)*num_create*num_src);-for(dst_cnt=i=0;i<rename_dst_nr;i++){-intbase=dst_cnt*num_src;-structdiff_filespec*two=rename_dst[i].two;-if(rename_dst[i].pair)-continue;/* dealt with exact match already. */-for(j=0;j<rename_src_nr;j++){-structdiff_filespec*one=rename_src[j].one;-structdiff_score*m=&mx[base+j];-m->src=j;-m->dst=i;-m->score=estimate_similarity(one,two,-minimum_score);-m->name_score=basename_same(one,two);-diff_free_filespec_blob(one);-}-/* We do not need the text anymore */-diff_free_filespec_blob(two);-dst_cnt++;-}-/* cost matrix sorted by most to least similar pair */-qsort(mx,num_create*num_src,sizeof(*mx),score_compare);-for(i=0;i<num_create*num_src;i++){-structdiff_rename_dst*dst=&rename_dst[mx[i].dst];-if(dst->pair)-continue;/* already done, either exact or fuzzy. */-if(mx[i].score<minimum_score)-break;/* there is no more usable pair. */-record_rename_pair(mx[i].dst,mx[i].src,mx[i].score);-rename_count++;-}-free(mx);+find_approximate_renames(minimum_score);cleanup:/* At this point, we have found some renames and copies and they
- no improvement on smaller datasets. Running "git-whatchanged -M
--raw -l0" on the linux-2.6 repo takes about the same time with the
old and new code (presumably the algorithmic savings of the new code
are lost in a higher constant factor, so when n is small, it is a
wash).
Have you compared the results? IOW, does it find the *same* renames?
I'm a bit worried about the fact that you just pick a single (arbitrary)
src/dst per fingerprint. Yes, it should be limited, but that seems to be a
bit too *extremely* limited. But if it gives the same results in practice,
maybe nobody cares?
Linus
From: Junio C Hamano <hidden> Date: 2016-06-15 22:43:45
Linus Torvalds [off-list ref] writes:
On Tue, 30 Oct 2007, Jeff King wrote:
quoted
- no improvement on smaller datasets. Running "git-whatchanged -M
--raw -l0" on the linux-2.6 repo takes about the same time with the
old and new code (presumably the algorithmic savings of the new code
are lost in a higher constant factor, so when n is small, it is a
wash).
Have you compared the results? IOW, does it find the *same* renames?
I'm a bit worried about the fact that you just pick a single (arbitrary)
src/dst per fingerprint. Yes, it should be limited, but that seems to be a
bit too *extremely* limited. But if it gives the same results in practice,
maybe nobody cares?
If it always gives the same results in practice, obviously
nobody can even notice.
However, merging this series to 'pu' breaks rebase-merge test
t3402 among other things.
From: Jeff King <hidden> Date: 2016-06-15 22:43:45
On Mon, Oct 29, 2007 at 10:06:11PM -0700, Linus Torvalds wrote:
Have you compared the results? IOW, does it find the *same* renames?
From my limited testing, it generally finds the same pairs. However,
there are a number of renames that it _doesn't_ find, because they are
composed of "uninteresting" lines, dropping them below the minimum
score. Try (in git.git):
git-show --raw -M -l0 :/'Big tool rename'
with the old and new code. Pairs like Documentation/git-add-script.txt
-> Documentation/git-add.txt are not found, because the file is composed
almost entirely of boilerplate.
Moving the size normalization into the similarity engine should probably
fix that, and will let us compare old and new results more accurately.
I'll try to work on that.
I'm a bit worried about the fact that you just pick a single (arbitrary)
src/dst per fingerprint. Yes, it should be limited, but that seems to be a
bit too *extremely* limited. But if it gives the same results in practice,
maybe nobody cares?
Yes, I have not convinced myself yet that it's the right approach (but
it seemed like a good place to try first, for simplicity and speed). As
I noted, this approach seems to be a bit memory hungry on large, so I am
a bit concerned about increasing the size of the fingerprint_entry
structure. However, Andy's sampling approach might help fix that.
The current code also doesn't bother marking overflow, so common lines
get attributes to some random file (actually, worse than random: if a
bunch of files have the same common lines, _all_ of the lines will go to
the last file, which means we subtly favor renames from the end of the
input list). So probably it should be tested as-is, with an "overflow,
this line is too common to be interesting" bit, and with a small-ish
limit (I had at one point tried 5, but the implementation was naive and
too memory-hungry).
-Peff
From: Jeff King <hidden> Date: 2016-06-15 22:43:45
On Tue, Oct 30, 2007 at 01:29:22AM -0700, Junio C Hamano wrote:
If it always gives the same results in practice, obviously
nobody can even notice.
However, merging this series to 'pu' breaks rebase-merge test
t3402 among other things.
Yes, sorry, I meant to mention the test breakage in the cover letter
(which I think is just related to the size/score normalization). This is
not really meant for applying, but more for "this is taking me a lot
longer than I hoped, so here is what is happening and you might be
interested to comment." I'm not even sure it's pu material. :)
I will continue to refine it as I mentioned in the mail to Linus, but I
am open to suggestions.
-Peff
On Mon, Oct 29, 2007 at 10:06:11PM -0700, Linus Torvalds wrote:
quoted
Have you compared the results? IOW, does it find the *same* renames?
From my limited testing, it generally finds the same pairs. However,
there are a number of renames that it _doesn't_ find, because they are
composed of "uninteresting" lines, dropping them below the minimum
score. Try (in git.git):
git-show --raw -M -l0 :/'Big tool rename'
with the old and new code. Pairs like Documentation/git-add-script.txt
-> Documentation/git-add.txt are not found, because the file is composed
almost entirely of boilerplate.
Ok, that does imply to me that we cannot just drop boilerplate text,
because the fact is, lots of files contain boilerplate, but people still
think they are "similar".
We do actually depend on the similarity analysis being "good" - because it
matters a lot for things like merging. The old code was actually very
careful indeed, and while it didn't care about things like the exact
*ordering* of lines (ie moving functions around in the same file resulted
in the *exact* same fingerprint for the file!) it cared about everything
else.
Moving the size normalization into the similarity engine should probably
fix that, and will let us compare old and new results more accurately.
I'll try to work on that.
Hmm. I hope that is sufficient. But I suspect it may well not be.
Especially since you ignore boiler-plate lines for *some* files but not
others (ie it depends on which file you happen to find it in first).
Linus
From: Jeff King <hidden> Date: 2016-06-15 22:43:45
On Tue, Oct 30, 2007 at 08:38:24AM -0700, Linus Torvalds wrote:
quoted
with the old and new code. Pairs like Documentation/git-add-script.txt
-> Documentation/git-add.txt are not found, because the file is composed
almost entirely of boilerplate.
Ok, that does imply to me that we cannot just drop boilerplate text,
because the fact is, lots of files contain boilerplate, but people still
think they are "similar".
Well, the problem is that instead of just "dropping" boilerplate text,
we fail to count it as a similarity, but it still counts towards the
file size. It may be that just dropping it totally is the right thing
(in which case those renames _will_ turn up, because they will be filled
with identical non-boilerplate goodness).
Hmm. I hope that is sufficient. But I suspect it may well not be.
Especially since you ignore boiler-plate lines for *some* files but not
others (ie it depends on which file you happen to find it in first).
Yes, that part bothers me a little, so I think a "too common, ignore"
overflow flag would at least be better.
But I think the best thing to do now is for me to shut up and see what
the results look like with the tweaks I have mentioned.
-Peff
Well, the problem is that instead of just "dropping" boilerplate text,
we fail to count it as a similarity, but it still counts towards the
file size. It may be that just dropping it totally is the right thing
(in which case those renames _will_ turn up, because they will be filled
with identical non-boilerplate goodness).
Yeah, you may well be right, and the normalization of the scores will just
solve things.
Linus
Sorry I have been AWOL... I was going to try to work on this, but I
got abjectly sick (long story). But it's great to see this out.
On 10/29/07, Jeff King [off-list ref] wrote:
This is my first stab at faster rename handling based on Andy's code.
The patches are on top of next (to get Linus' recent work on exact
renames). Most of the interesting stuff is in 2/3.
1/3: extension of hash interface
2/3: similarity detection code
3/3: integrate similarity detection into diffcore-rename
The implementation is pretty basic, so I think there is room for
code optimization (50% of the time is spent in hash lookups, so we might
be able to micro-optimize that) as well as algorithmic improvements (like the
sampling Andy mentioned).
For microoptimization, I was thinking that the hash tables could be
implemented without pointers per value (or memory allocation per
value), so everything is in a contiguous block of memory. In C++ you
can do this trivially by declaring a small struct as the second
template parameter of the container; in C I guess you can simulate it
with a macro or something.
For the inverted indexing step, the values in the hash are going to be
quite small, especially if line_threshold=1. Then you only need 2
integers for the left side and right side == 4 integers. The integers
could just be indexes into the lists (like the current code uses).
For the count matrix step, the values are just going to be integers,
so storing it right in the hash table makes sense.
The sampling should be only necessary for binary files, I think.
With these patches, I can get my monster binary diff down from about 2
minutes to 17 seconds. And comparing all of linux-2.4 to all of
linux-2.6 (similar to Andy's previous demo) takes about 10 seconds.
Hopefully that should be close to just reading the files off disk.
The algorithm should take a fraction of the time that simply reading
the files does, which presumably a git diff has to do.
I was timing that by comparing it to doing a "| xargs wc -l" on the
lists of files.
There are a few downsides:
- the current implementation tends to give lower similarity values
compared to the old code (see discussion in 2/3), but this should be
tweakable
- on large datasets, it's more memory hungry than the old code because
the hash grows very large. This can be helped by bumping up the
binary chunk size (actually, the 17 seconds quoted above is using
256-byte chunks rather than 64-byte -- with 64-byte chunks, it's
more like 24 seconds) as well as sampling.
- no improvement on smaller datasets. Running "git-whatchanged -M
--raw -l0" on the linux-2.6 repo takes about the same time with the
old and new code (presumably the algorithmic savings of the new code
are lost in a higher constant factor, so when n is small, it is a
wash).
I think the old code tries to respect the cache as much as possible,
from what I can tell. The new code has to use hash tables which are
unpredictable of course. Though for smaller data sets I would expect
the hash table to fit in cache. What's your definition of small here?
Are you sure the old code isn't triggering one of the limits that was
there?
thanks,
Andy
On Tue, Oct 30, 2007 at 08:38:24AM -0700, Linus Torvalds wrote:
quoted
quoted
with the old and new code. Pairs like Documentation/git-add-script.txt
-> Documentation/git-add.txt are not found, because the file is composed
almost entirely of boilerplate.
Ok, that does imply to me that we cannot just drop boilerplate text,
because the fact is, lots of files contain boilerplate, but people still
think they are "similar".
Well, the problem is that instead of just "dropping" boilerplate text,
we fail to count it as a similarity, but it still counts towards the
file size. It may be that just dropping it totally is the right thing
(in which case those renames _will_ turn up, because they will be filled
with identical non-boilerplate goodness).
Right, in the demo I make an extra pass after the inverted indexing
step to prune the index -- which means eliminating the common lines
*entirely* from the index (so they don't get attributed to a random
file) *and* decrementing all the file sizes by 1. That way the
similarity scores shouldn't get skewed.
And as you mentioned we could bump the threshold from 1 to some other
small integer. Intuitively I guess you could say it is common to copy
a file to 2 places or 3 places, and you don't want all the lines to
get thrown out because of that. But usually you don't copy a file to
10 or 50 places.
Andy