Skip to content

Commit cf812c0

Browse files
committed
matchfinder: add M0
M0 is a MatchFinder based on the algorithm for brotli level 0.
1 parent 1b6cf36 commit cf812c0

File tree

2 files changed

+185
-0
lines changed

2 files changed

+185
-0
lines changed

brotli_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,3 +693,19 @@ func BenchmarkEncodeM4Chain64(b *testing.B) {
693693
func BenchmarkEncodeM4Chain128(b *testing.B) {
694694
benchmark(b, "testdata/Isaac.Newton-Opticks.txt", &matchfinder.M4{MaxDistance: 1 << 20, ChainLength: 128, HashLen: 5, DistanceBitCost: 57}, 1<<16)
695695
}
696+
697+
func TestEncodeM0(t *testing.T) {
698+
test(t, "testdata/Isaac.Newton-Opticks.txt", matchfinder.M0{}, 1<<16)
699+
}
700+
701+
func BenchmarkEncodeM0(b *testing.B) {
702+
benchmark(b, "testdata/Isaac.Newton-Opticks.txt", matchfinder.M0{}, 1<<16)
703+
}
704+
705+
func TestEncodeM0Lazy(t *testing.T) {
706+
test(t, "testdata/Isaac.Newton-Opticks.txt", matchfinder.M0{Lazy: true}, 1<<16)
707+
}
708+
709+
func BenchmarkEncodeM0Lazy(b *testing.B) {
710+
benchmark(b, "testdata/Isaac.Newton-Opticks.txt", matchfinder.M0{Lazy: true}, 1<<16)
711+
}

matchfinder/m0.go

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
package matchfinder
2+
3+
import (
4+
"encoding/binary"
5+
)
6+
7+
// M0 is an implementation of the MatchFinder interface based
8+
// on the algorithm used by snappy, but modified to be more like the algorithm
9+
// used by compression level 0 of the brotli reference implementation.
10+
//
11+
// It has a maximum block size of 65536 bytes.
12+
type M0 struct {
13+
// Lazy turns on "lazy matching," for higher compression but less speed.
14+
Lazy bool
15+
16+
MaxDistance int
17+
MaxLength int
18+
}
19+
20+
func (M0) Reset() {}
21+
22+
const (
23+
m0HashLen = 5
24+
25+
m0TableBits = 14
26+
m0TableSize = 1 << m0TableBits
27+
m0Shift = 32 - m0TableBits
28+
// m0TableMask is redundant, but helps the compiler eliminate bounds
29+
// checks.
30+
m0TableMask = m0TableSize - 1
31+
)
32+
33+
func (m M0) hash(data uint64) uint64 {
34+
hash := (data << (64 - 8*m0HashLen)) * hashMul64
35+
return hash >> (64 - m0TableBits)
36+
}
37+
38+
// FindMatches looks for matches in src, appends them to dst, and returns dst.
39+
// src must not be longer than 65536 bytes.
40+
func (m M0) FindMatches(dst []Match, src []byte) []Match {
41+
const inputMargin = 16 - 1
42+
const minNonLiteralBlockSize = 1 + 1 + inputMargin
43+
44+
if len(src) < minNonLiteralBlockSize {
45+
dst = append(dst, Match{
46+
Unmatched: len(src),
47+
})
48+
return dst
49+
}
50+
if len(src) > 65536 {
51+
panic("block too long")
52+
}
53+
54+
var table [m0TableSize]uint16
55+
56+
// sLimit is when to stop looking for offset/length copies. The inputMargin
57+
// lets us use a fast path for emitLiteral in the main loop, while we are
58+
// looking for copies.
59+
sLimit := len(src) - inputMargin
60+
61+
// nextEmit is where in src the next emitLiteral should start from.
62+
nextEmit := 0
63+
64+
// The encoded form must start with a literal, as there are no previous
65+
// bytes to copy, so we start looking for hash matches at s == 1.
66+
s := 1
67+
nextHash := m.hash(binary.LittleEndian.Uint64(src[s:]))
68+
69+
for {
70+
// Copied from the C++ snappy implementation:
71+
//
72+
// Heuristic match skipping: If 32 bytes are scanned with no matches
73+
// found, start looking only at every other byte. If 32 more bytes are
74+
// scanned (or skipped), look at every third byte, etc.. When a match
75+
// is found, immediately go back to looking at every byte. This is a
76+
// small loss (~5% performance, ~0.1% density) for compressible data
77+
// due to more bookkeeping, but for non-compressible data (such as
78+
// JPEG) it's a huge win since the compressor quickly "realizes" the
79+
// data is incompressible and doesn't bother looking for matches
80+
// everywhere.
81+
//
82+
// The "skip" variable keeps track of how many bytes there are since
83+
// the last match; dividing it by 32 (ie. right-shifting by five) gives
84+
// the number of bytes to move ahead for each iteration.
85+
skip := 32
86+
87+
nextS := s
88+
candidate := 0
89+
for {
90+
s = nextS
91+
bytesBetweenHashLookups := skip >> 5
92+
nextS = s + bytesBetweenHashLookups
93+
skip += bytesBetweenHashLookups
94+
if nextS > sLimit {
95+
goto emitRemainder
96+
}
97+
candidate = int(table[nextHash&m0TableMask])
98+
table[nextHash&m0TableMask] = uint16(s)
99+
nextHash = m.hash(binary.LittleEndian.Uint64(src[nextS:]))
100+
if m.MaxDistance != 0 && s-candidate > m.MaxDistance {
101+
continue
102+
}
103+
if binary.LittleEndian.Uint32(src[s:]) == binary.LittleEndian.Uint32(src[candidate:]) {
104+
break
105+
}
106+
}
107+
108+
// Invariant: we have a 4-byte match at s.
109+
base := s
110+
s = extendMatch(src, candidate+4, s+4)
111+
112+
origBase := base
113+
if m.Lazy && base+1 < sLimit {
114+
newBase := base + 1
115+
h := m.hash(binary.LittleEndian.Uint64(src[newBase:]))
116+
newCandidate := int(table[h&m0TableMask])
117+
table[h&m0TableMask] = uint16(newBase)
118+
okDistance := true
119+
if m.MaxDistance != 0 && newBase-newCandidate > m.MaxDistance {
120+
okDistance = false
121+
}
122+
if okDistance && binary.LittleEndian.Uint32(src[newBase:]) == binary.LittleEndian.Uint32(src[newCandidate:]) {
123+
newS := extendMatch(src, newCandidate+4, newBase+4)
124+
if newS-newBase > s-base+1 {
125+
s = newS
126+
base = newBase
127+
candidate = newCandidate
128+
}
129+
}
130+
}
131+
132+
if m.MaxLength != 0 && s-base > m.MaxLength {
133+
s = base + m.MaxLength
134+
}
135+
dst = append(dst, Match{
136+
Unmatched: base - nextEmit,
137+
Length: s - base,
138+
Distance: base - candidate,
139+
})
140+
nextEmit = s
141+
if s >= sLimit {
142+
goto emitRemainder
143+
}
144+
145+
if m.Lazy {
146+
// If lazy matching is enabled, we update the hash table for
147+
// every byte in the match.
148+
for i := origBase + 2; i < s-1; i++ {
149+
x := binary.LittleEndian.Uint64(src[i:])
150+
table[m.hash(x)&m0TableMask] = uint16(i)
151+
}
152+
}
153+
154+
// We could immediately start working at s now, but to improve
155+
// compression we first update the hash table at s-1 and at s.
156+
x := binary.LittleEndian.Uint64(src[s-1:])
157+
prevHash := m.hash(x >> 0)
158+
table[prevHash&m0TableMask] = uint16(s - 1)
159+
nextHash = m.hash(x >> 8)
160+
}
161+
162+
emitRemainder:
163+
if nextEmit < len(src) {
164+
dst = append(dst, Match{
165+
Unmatched: len(src) - nextEmit,
166+
})
167+
}
168+
return dst
169+
}

0 commit comments

Comments
 (0)