Commit 53efe1e1 authored by Klaus Post's avatar Klaus Post Committed by Nigel Tao

compress/flate: rework matching algorithm

This changes how matching is done in deflate algorithm.

The major change is that we do not look for matches that are only
3 bytes in length, matches must be 4 bytes at least.
Contrary to what you would expect this actually improves the
compresion ratio, since 3 literal bytes will often be shorter
than a match after huffman encoding.
This varies a bit by source, but is most often the case when the
source is "easy" to compress.

Second of all, a "stronger" hash is used. The hash is similar to
the hashing function used by Snappy.

Overall, the speed impact is biggest on higher compression levels.
I intend to replace the "speed" compression level, which can be
seen in CL 21021.

The built-in benchmark using "digits" is slower at level 1.
I see this as an exception, since "digits" is a special type
of data, where you have low entropy (numbers 0->9), but no
significant matches. Again, CL 20021 fixes that case.

NewWriterDict is also made considerably faster, by not running data
through the entire encoder. This is not reflected by the benchmark.

Overall, the speed impact is biggest on higher compression levels.
I intend to replace the "speed" compression level.

COMPARED to tip/master:
name                       old time/op    new time/op     delta
EncodeDigitsSpeed1e4-4        401µs ± 1%      345µs ± 2%   -13.95%
EncodeDigitsSpeed1e5-4       3.19ms ± 1%     4.27ms ± 3%   +33.96%
EncodeDigitsSpeed1e6-4       27.7ms ± 4%     43.8ms ± 3%   +58.00%
EncodeDigitsDefault1e4-4      641µs ± 0%      403µs ± 1%   -37.15%
EncodeDigitsDefault1e5-4     13.8ms ± 1%      6.4ms ± 3%   -53.73%
EncodeDigitsDefault1e6-4      162ms ± 1%       64ms ± 2%   -60.51%
EncodeDigitsCompress1e4-4     627µs ± 1%      405µs ± 2%   -35.45%
EncodeDigitsCompress1e5-4    13.9ms ± 0%      6.3ms ± 2%   -54.46%
EncodeDigitsCompress1e6-4     159ms ± 1%       64ms ± 0%   -59.91%
EncodeTwainSpeed1e4-4         433µs ± 4%      331µs ± 1%   -23.53%
EncodeTwainSpeed1e5-4        2.82ms ± 1%     3.08ms ± 0%    +9.10%
EncodeTwainSpeed1e6-4        28.1ms ± 2%     28.8ms ± 0%    +2.82%
EncodeTwainDefault1e4-4       695µs ± 4%      474µs ± 1%   -31.78%
EncodeTwainDefault1e5-4      11.8ms ± 0%      7.4ms ± 0%   -37.31%
EncodeTwainDefault1e6-4       128ms ± 0%       75ms ± 0%   -40.93%
EncodeTwainCompress1e4-4      719µs ± 3%      480µs ± 0%   -33.27%
EncodeTwainCompress1e5-4     15.0ms ± 3%      8.2ms ± 2%   -45.55%
EncodeTwainCompress1e6-4      170ms ± 0%       85ms ± 1%   -49.99%

name                       old speed      new speed       delta
EncodeDigitsSpeed1e4-4     25.0MB/s ± 1%   29.0MB/s ± 2%   +16.24%
EncodeDigitsSpeed1e5-4     31.4MB/s ± 1%   23.4MB/s ± 3%   -25.34%
EncodeDigitsSpeed1e6-4     36.1MB/s ± 4%   22.8MB/s ± 3%   -36.74%
EncodeDigitsDefault1e4-4   15.6MB/s ± 0%   24.8MB/s ± 1%   +59.11%
EncodeDigitsDefault1e5-4   7.27MB/s ± 1%  15.72MB/s ± 3%  +116.23%
EncodeDigitsDefault1e6-4   6.16MB/s ± 0%  15.60MB/s ± 2%  +153.25%
EncodeDigitsCompress1e4-4  15.9MB/s ± 1%   24.7MB/s ± 2%   +54.97%
EncodeDigitsCompress1e5-4  7.19MB/s ± 0%  15.78MB/s ± 2%  +119.62%
EncodeDigitsCompress1e6-4  6.27MB/s ± 1%  15.65MB/s ± 0%  +149.52%
EncodeTwainSpeed1e4-4      23.1MB/s ± 4%   30.2MB/s ± 1%   +30.68%
EncodeTwainSpeed1e5-4      35.4MB/s ± 1%   32.5MB/s ± 0%    -8.34%
EncodeTwainSpeed1e6-4      35.6MB/s ± 2%   34.7MB/s ± 0%    -2.77%
EncodeTwainDefault1e4-4    14.4MB/s ± 4%   21.1MB/s ± 1%   +46.48%
EncodeTwainDefault1e5-4    8.49MB/s ± 0%  13.55MB/s ± 0%   +59.50%
EncodeTwainDefault1e6-4    7.83MB/s ± 0%  13.25MB/s ± 0%   +69.19%
EncodeTwainCompress1e4-4   13.9MB/s ± 3%   20.8MB/s ± 0%   +49.83%
EncodeTwainCompress1e5-4   6.65MB/s ± 3%  12.20MB/s ± 2%   +83.51%
EncodeTwainCompress1e6-4   5.88MB/s ± 0%  11.76MB/s ± 1%  +100.06%

Change-Id: I724e33c1dd3e3a6a1b0a68e094baa959352baf32
Reviewed-on: https://go-review.googlesource.com/20929
Run-TryBot: Nigel Tao <nigeltao@golang.org>
Reviewed-by: 's avatarNigel Tao <nigeltao@golang.org>
parent e6beec1f
This diff is collapsed.
...@@ -80,6 +80,32 @@ func largeDataChunk() []byte { ...@@ -80,6 +80,32 @@ func largeDataChunk() []byte {
return result return result
} }
func TestBulkHash4(t *testing.T) {
for _, x := range deflateTests {
y := x.out
if len(y) < minMatchLength {
continue
}
y = append(y, y...)
for j := 4; j < len(y); j++ {
y := y[:j]
dst := make([]uint32, len(y)-minMatchLength+1)
for i := range dst {
dst[i] = uint32(i + 100)
}
bulkHash4(y, dst)
for i, got := range dst {
want := hash4(y[i:])
if got != want && got == uint32(i)+100 {
t.Errorf("Len:%d Index:%d, want 0x%08x but not modified", len(y), i, want)
} else if got != want {
t.Errorf("Len:%d Index:%d, got 0x%08x want:0x%08x", len(y), i, got, want)
}
}
}
}
}
func TestDeflate(t *testing.T) { func TestDeflate(t *testing.T) {
for _, h := range deflateTests { for _, h := range deflateTests {
var buf bytes.Buffer var buf bytes.Buffer
...@@ -91,7 +117,7 @@ func TestDeflate(t *testing.T) { ...@@ -91,7 +117,7 @@ func TestDeflate(t *testing.T) {
w.Write(h.in) w.Write(h.in)
w.Close() w.Close()
if !bytes.Equal(buf.Bytes(), h.out) { if !bytes.Equal(buf.Bytes(), h.out) {
t.Errorf("Deflate(%d, %x) = %x, want %x", h.level, h.in, buf.Bytes(), h.out) t.Errorf("Deflate(%d, %x) = \n%#v, want \n%#v", h.level, h.in, buf.Bytes(), h.out)
} }
} }
} }
...@@ -289,6 +315,9 @@ func testToFromWithLevelAndLimit(t *testing.T, level int, input []byte, name str ...@@ -289,6 +315,9 @@ func testToFromWithLevelAndLimit(t *testing.T, level int, input []byte, name str
t.Errorf("level: %d, len(compress(data)) = %d > limit = %d", level, buffer.Len(), limit) t.Errorf("level: %d, len(compress(data)) = %d > limit = %d", level, buffer.Len(), limit)
return return
} }
if limit > 0 {
t.Logf("level: %d, size:%.2f%%, %d b\n", level, float64(buffer.Len()*100)/float64(limit), buffer.Len())
}
r := NewReader(&buffer) r := NewReader(&buffer)
out, err := ioutil.ReadAll(r) out, err := ioutil.ReadAll(r)
if err != nil { if err != nil {
...@@ -457,6 +486,17 @@ func TestWriterReset(t *testing.T) { ...@@ -457,6 +486,17 @@ func TestWriterReset(t *testing.T) {
// DeepEqual doesn't compare functions. // DeepEqual doesn't compare functions.
w.d.fill, wref.d.fill = nil, nil w.d.fill, wref.d.fill = nil, nil
w.d.step, wref.d.step = nil, nil w.d.step, wref.d.step = nil, nil
w.d.bulkHasher, wref.d.bulkHasher = nil, nil
// hashMatch is always overwritten when used.
copy(w.d.hashMatch[:], wref.d.hashMatch[:])
if len(w.d.tokens) != 0 {
t.Errorf("level %d Writer not reset after Reset. %d tokens were present", level, len(w.d.tokens))
}
// As long as the length is 0, we don't care about the content.
w.d.tokens = wref.d.tokens
// We don't care if there are values in the window, as long as it is at d.index is 0
w.d.window = wref.d.window
if !reflect.DeepEqual(w, wref) { if !reflect.DeepEqual(w, wref) {
t.Errorf("level %d Writer not reset after Reset", level) t.Errorf("level %d Writer not reset after Reset", level)
} }
...@@ -481,7 +521,7 @@ func testResetOutput(t *testing.T, newWriter func(w io.Writer) (*Writer, error)) ...@@ -481,7 +521,7 @@ func testResetOutput(t *testing.T, newWriter func(w io.Writer) (*Writer, error))
w.Write(b) w.Write(b)
} }
w.Close() w.Close()
out1 := buf.String() out1 := buf.Bytes()
buf2 := new(bytes.Buffer) buf2 := new(bytes.Buffer)
w.Reset(buf2) w.Reset(buf2)
...@@ -489,10 +529,23 @@ func testResetOutput(t *testing.T, newWriter func(w io.Writer) (*Writer, error)) ...@@ -489,10 +529,23 @@ func testResetOutput(t *testing.T, newWriter func(w io.Writer) (*Writer, error))
w.Write(b) w.Write(b)
} }
w.Close() w.Close()
out2 := buf2.String() out2 := buf2.Bytes()
if out1 != out2 { if len(out1) != len(out2) {
t.Errorf("got %q, expected %q", out2, out1) t.Errorf("got %d, expected %d bytes", len(out2), len(out1))
return
}
if !bytes.Equal(out1, out2) {
mm := 0
for i, b := range out1[:len(out2)] {
if b != out2[i] {
t.Errorf("mismatch index %d: %#02x, expected %#02x", i, out2[i], b)
}
mm++
if mm == 10 {
t.Fatal("Stopping")
}
}
} }
t.Logf("got %d bytes", len(out1)) t.Logf("got %d bytes", len(out1))
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment