From: Junio C Hamano <hidden> Date: 2016-06-15 22:43:37
Jeff King [off-list ref] writes:
Just to update, I tried using a non-colliding hash for this (at the
expense of much memory), and I wasn't able to get things much faster
(and certainly not worth the explosion in memory), short of reducing the
size of the hash (which is going to reduce the quality of the output).
So I am giving up for the time being, but if others are interested in
trying to speed things up, I would be happy to discuss ideas.
Bummer. You are giving up at the same place I gave up the last
time. I was somehow hoping that other people are more clever
and determined than I was ;-).
Thanks for trying.
From: Jeff King <hidden> Date: 2016-06-15 22:43:37
On Mon, Oct 01, 2007 at 10:01:16PM -0700, Junio C Hamano wrote:
quoted
Just to update, I tried using a non-colliding hash for this (at the
expense of much memory), and I wasn't able to get things much faster
(and certainly not worth the explosion in memory), short of reducing the
size of the hash (which is going to reduce the quality of the output).
So I am giving up for the time being, but if others are interested in
trying to speed things up, I would be happy to discuss ideas.
Bummer. You are giving up at the same place I gave up the last
time. I was somehow hoping that other people are more clever
and determined than I was ;-).
Thanks for trying.
What was so discouraging is that I literally simplified the process to
for(i = 0; i < HASH_SIZE; i++)
if(src[i] < dst[i])
...
and it spent all of the time on that one conditional.
One approach which I haven't tried but might be promising is to actually
keep each list sorted, and then do a "merge" of the two lists, comparing
as you go. We don't really need to do arbitrary lookups in the hash; we
just need to compare two hash tables at a time. My approach was to be
simple, but have O(HASH_SIZE) comparisons (where HASH_SIZE is on the
order of 2^17), and that's clearly just too big. But with a list merge,
it should be O(n), where n is the actual number of lines in the files
(or binary chunks for the binary case).
-Peff
From: Jeff King <hidden> Date: 2016-06-15 22:43:37
On Tue, Oct 02, 2007 at 01:08:20AM -0400, Jeff King wrote:
One approach which I haven't tried but might be promising is to actually
keep each list sorted, and then do a "merge" of the two lists, comparing
as you go. We don't really need to do arbitrary lookups in the hash; we
just need to compare two hash tables at a time. My approach was to be
simple, but have O(HASH_SIZE) comparisons (where HASH_SIZE is on the
order of 2^17), and that's clearly just too big. But with a list merge,
it should be O(n), where n is the actual number of lines in the files
(or binary chunks for the binary case).
BTW, I don't want to steal credit for this idea...it comes from thinking
about what David Kastrup said earlier in the thread, though I think he
was proposing sorting just inside buckets.
-Peff
From: David Kastrup <hidden> Date: 2016-06-15 22:43:37
Jeff King [off-list ref] writes:
On Tue, Oct 02, 2007 at 01:08:20AM -0400, Jeff King wrote:
quoted
One approach which I haven't tried but might be promising is to actually
keep each list sorted, and then do a "merge" of the two lists, comparing
as you go. We don't really need to do arbitrary lookups in the hash; we
just need to compare two hash tables at a time. My approach was to be
simple, but have O(HASH_SIZE) comparisons (where HASH_SIZE is on the
order of 2^17), and that's clearly just too big. But with a list merge,
it should be O(n), where n is the actual number of lines in the files
(or binary chunks for the binary case).
BTW, I don't want to steal credit for this idea...it comes from thinking
about what David Kastrup said earlier in the thread, though I think he
was proposing sorting just inside buckets.
Yes: my proposal was about a microoptimization: work with the
basically existing data structures and put the already contained
information to best use.
I have not actually looked at the actual task that the structures are
going to be used in, and whether "reusing" the information is likely
to be worth the trouble.
When we are talking about buzzword compliance, "keep sorted" with the
meaning of "maintain sorted across modifications" has an O(n^2) or at
least O(nm) ring to it. However, if it is possible to sort it just
once, and then then only merge with other lists...
I am actually quite a fan of merge sort and have even posted a small
and quite efficient version to this list once. However, merge sorts
were really greatest at the time when cache memory was unusual to
have. Nowadays, quicksort or similar could be faster due to better
locality of memory accesses. I think the glibc qsort more or less
uses an array-based merge into a separate memory area (unless it runs
out of memory in which case it resorts to regular quicksort).
--
David Kastrup
From: Jeff King <hidden> Date: 2016-06-15 22:43:37
On Tue, Oct 02, 2007 at 08:10:28AM +0200, David Kastrup wrote:
I have not actually looked at the actual task that the structures are
going to be used in, and whether "reusing" the information is likely
to be worth the trouble.
The algorithm is something like this: We have N files, and we want to
find "similar" candidates. So we go through each file and generate a
table of fingperint hashes (diffcore-rename.c:hash_chars), and then
compare each file with every other file, using the hash tables to do the
comparison.
So the comparison step for two files is currently something like:
for each hash in file1
hash2 = look up hash in file2
compare hash and hash2
and if they were sorted, perhaps we could do something merge-like:
while hashes are left to compare
compare file1.next, file2.next
advance file1, file2, or both (depending on comparison)
When we are talking about buzzword compliance, "keep sorted" with the
meaning of "maintain sorted across modifications" has an O(n^2) or at
least O(nm) ring to it. However, if it is possible to sort it just
once, and then then only merge with other lists...
It would be sort once. I.e.,:
for each file
generate file.hashes
sort file.hashes
for each file1
for each file2
compare file1.hashes to file2.hashes
where that 'compare' step is taking most of the CPU time (for the
obvious reason that we call it in an O(n^2) loop).
I will try to implement this as time permits, but if you want to tinker
with it in the meantime, feel free.
-Peff
From: David Kastrup <hidden> Date: 2016-06-15 22:43:37
Jeff King [off-list ref] writes:
On Tue, Oct 02, 2007 at 08:10:28AM +0200, David Kastrup wrote:
[...]
The algorithm is something like this: We have N files, and we want
to find "similar" candidates. So we go through each file and
generate a table of fingperint hashes
(diffcore-rename.c:hash_chars), and then compare each file with
every other file, using the hash tables to do the comparison.
So the comparison step for two files is currently something like:
for each hash in file1
hash2 = look up hash in file2
compare hash and hash2
and if they were sorted, perhaps we could do something merge-like:
while hashes are left to compare
compare file1.next, file2.next
advance file1, file2, or both (depending on comparison)
quoted
When we are talking about buzzword compliance, "keep sorted" with
the meaning of "maintain sorted across modifications" has an O(n^2)
or at least O(nm) ring to it. However, if it is possible to sort
it just once, and then then only merge with other lists...
It would be sort once. I.e.,:
for each file
generate file.hashes
sort file.hashes
for each file1
for each file2
compare file1.hashes to file2.hashes
where that 'compare' step is taking most of the CPU time (for the
obvious reason that we call it in an O(n^2) loop).
I will try to implement this as time permits, but if you want to
tinker with it in the meantime, feel free.
This does not actually require an actual merge _sort_ AFAICS: do the
"sort file.hashed" step using qsort. The comparison step does not
actually need to produce merged output, but merely advances through
two hash arrays and generates statistics.
This should already beat the pants off the current implementation,
even when the hash array is sparse, simply because our inner loop then
has perfect hash coherence.
Getting rid of this outer O(n^2) remains an interesting challenge,
though. One way would be the following: fill a _single_ array with
entries containing _both_ hash and file number. Sort this, and then
gather the statistics of hash runs by making a single pass through.
That reduces the O(n^2) behavior to only those parts with actual hash
collisions.
--
David Kastrup
From: Jeff King <hidden> Date: 2016-06-15 22:43:37
On Tue, Oct 02, 2007 at 06:31:18PM +0200, David Kastrup wrote:
This does not actually require an actual merge _sort_ AFAICS: do the
"sort file.hashed" step using qsort. The comparison step does not
actually need to produce merged output, but merely advances through
two hash arrays and generates statistics.
Right, that's why I used "merge" in quotes. The sort used in the O(n)
step is irrelevant, but we are doing a merge-sort-like behavior in the
second step (except instead of actually merging into a new list, we are
summarizing the comparisons in a numeric "difference" variable). But I
think we are on the same page.
This should already beat the pants off the current implementation,
even when the hash array is sparse, simply because our inner loop then
has perfect hash coherence.
Yes, I hope so. We'll see. :)
Getting rid of this outer O(n^2) remains an interesting challenge,
though. One way would be the following: fill a _single_ array with
entries containing _both_ hash and file number. Sort this, and then
gather the statistics of hash runs by making a single pass through.
That reduces the O(n^2) behavior to only those parts with actual hash
collisions.
Interesting. Care to take a stab at implementing it?
-Peff
[ This is the discussed stupid approach - just sort the dang hash array,
so that we can use a linear scan over the src/dst ]
On Tue, 2 Oct 2007, David Kastrup wrote:
This does not actually require an actual merge _sort_ AFAICS: do the
"sort file.hashed" step using qsort. The comparison step does not
actually need to produce merged output, but merely advances through
two hash arrays and generates statistics.
This should already beat the pants off the current implementation,
even when the hash array is sparse, simply because our inner loop then
has perfect hash coherence.
Sadly, that's not the case. It *does* seem to beat the current
implementation, but it's not "beat the pants off". It looks like an
improvement of about 15%, which is nothing to sneeze at, but it's not an
order-of-magnitude improvement either.
Here's a test-patch. I don't guarantee anything, except that when I did
the timings I also did a "wc" on the result, and they matched..
Before:
[torvalds@woody linux]$ time git diff -l0 --stat -C v2.6.22.. | wc
7104 28574 438020
real 0m10.526s
user 0m10.401s
sys 0m0.136s
After:
[torvalds@woody linux]$ time ~/git/git diff -l0 --stat -C v2.6.22.. | wc
7104 28574 438020
real 0m8.876s
user 0m8.761s
sys 0m0.128s
but the diff is fairly simple, so if somebody will go over it and say
whether it's likely to be *correct* too, that 15% may well be worth it.
[ Side note, without rename detection, that diff takes just under three
seconds for me, so in that sense the improvement to the rename detection
itself is larger than the overall 15% - it brings the cost of just
rename detection from 7.5s to 5.9s, which would be on the order of just
over a 20% performance improvement. ]
Hmm. The patch depends on half-way subtle issues like the fact that the
hashtables are guaranteed to not be full => we're guaranteed to have zero
counts at the end => we don't need to do any steenking iterator count in
the loop. A few comments might in order.
Linus
---
diffcore-delta.c | 54 ++++++++++++++++++++++++++++++------------------------
1 files changed, 30 insertions(+), 24 deletions(-)
@@ -122,6 +106,20 @@ static struct spanhash_top *add_spanhash(struct spanhash_top *top,}}+staticintspanhash_cmp(constvoid*_a,constvoid*_b)+{+conststructspanhash*a=_a;+conststructspanhash*b=_b;++/* A count of zero compares at the end.. */+if(!a->cnt)+return!b->cnt?0:1;+if(!b->cnt)+return-1;+returna->hashval<b->hashval?-1:+a->hashval>b->hashval?1:0;+}+staticstructspanhash_top*hash_chars(structdiff_filespec*one){inti,n;
From: Jeff King <hidden> Date: 2016-06-15 22:43:38
On Tue, Oct 02, 2007 at 07:28:19PM -0700, Linus Torvalds wrote:
Sadly, that's not the case. It *does* seem to beat the current
implementation, but it's not "beat the pants off". It looks like an
improvement of about 15%, which is nothing to sneeze at, but it's not an
order-of-magnitude improvement either.
Here's a test-patch. I don't guarantee anything, except that when I did
the timings I also did a "wc" on the result, and they matched..
I get slightly better speedups with my pathological case (around 30%):
Before:
$ /usr/bin/time git-diff --raw -M -l0 06d288^ 06d288 >/dev/null
105.38user 3.65system 2:14.90elapsed 80%CPU (0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (15432major+542627minor)pagefaults 0swaps
After:
$ /usr/bin/time git-diff --raw -M -l0 06d288^ 06d288 >/dev/null
71.70user 3.47system 1:40.43elapsed 74%CPU (0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (15065major+551778minor)pagefaults 0swaps
But yes, it's not the order of magnitude we were looking for.
I found less noise in the timing by using --raw, since the patch
computation takes an appreciable amount of time.
but the diff is fairly simple, so if somebody will go over it and say
whether it's likely to be *correct* too, that 15% may well be worth it.
Patch looks correct, and it produces correct results on my (admittedly
limited) test data.
I think it's worth applying (though I agree that a comment on the
assumption of a zero "cnt" at the end is worth adding) unless some
drastically different solution comes along (e.g., David's idea to try
avoiding the outer O(n^2) loop). But I don't think there is much more to
be gained from a different approach to comparing the two hash tables.
-Peff
I get slightly better speedups with my pathological case (around 30%):
Ok, 30% is definitely "worth doing". Even if your performance still sucks,
and 71 seconds is just way out of line for anything like this (of course,
these days you need that "-l0" to ever trigger that case, but it would be
nice if we could speed things up so much that we no longer care).
Linus