Skip to content

Commit 45be353

Browse files
committed
index/suffixarray: add 32-bit implementation
The original index/suffixarray used 32-bit ints on 64-bit machines, because that's what 'int' meant in Go at the time. When we changed the meaning of int, that doubled the space overhead of suffix arrays for all uses, even though the vast majority of them describe less than 2 GB of text. The space overhead of a suffix array compared to the text is not insignificant: there's a big difference for many uses between 4X and 8X. This CL adjusts names in qsufsort.go so that a global search and replace s/32/64/g produces a working 64-bit implementation, and then it modifies suffixarray.go to choose between the 32-bit and 64-bit implementation as appropriate depending on the input size. The 64-bit implementation is generated by 'go generate'. This CL also restructures the benchmarks, to test different input sizes, different input texts, and 32-bit vs 64-bit. The serialized form uses varint-encoded numbers and is unchanged, so on-disk suffix arrays written by older versions of Go will be readable by this version, and vice versa. The 32-bit version runs a up to 17% faster than the 64-bit version on real inputs, but more importantly it uses 50% less memory. I have a followup CL that also implements a faster algorithm on top of these improvements, but these are a good first step. name 64-bit speed 32-bit speed delta New/text=opticks/size=100K/bits=*-12 4.44MB/s ± 0% 4.64MB/s ± 0% +4.41% (p=0.008 n=5+5) New/text=opticks/size=500K/bits=*-12 3.70MB/s ± 1% 3.82MB/s ± 0% +3.30% (p=0.008 n=5+5) New/text=go/size=100K/bits=*-12 4.40MB/s ± 0% 4.61MB/s ± 0% +4.82% (p=0.008 n=5+5) New/text=go/size=500K/bits=*-12 3.66MB/s ± 0% 3.77MB/s ± 0% +3.01% (p=0.016 n=4+5) New/text=go/size=1M/bits=*-12 3.29MB/s ± 0% 3.55MB/s ± 0% +7.90% (p=0.016 n=5+4) New/text=go/size=5M/bits=*-12 2.25MB/s ± 1% 2.65MB/s ± 0% +17.81% (p=0.008 n=5+5) New/text=go/size=10M/bits=*-12 1.82MB/s ± 0% 2.09MB/s ± 1% +14.36% (p=0.008 n=5+5) New/text=go/size=50M/bits=*-12 1.35MB/s ± 0% 1.51MB/s ± 1% +12.33% (p=0.008 n=5+5) New/text=zero/size=100K/bits=*-12 3.42MB/s ± 0% 3.32MB/s ± 0% -2.74% (p=0.000 n=5+4) New/text=zero/size=500K/bits=*-12 3.00MB/s ± 1% 2.97MB/s ± 0% -1.13% (p=0.016 n=5+4) New/text=zero/size=1M/bits=*-12 2.81MB/s ± 0% 2.78MB/s ± 2% ~ (p=0.167 n=5+5) New/text=zero/size=5M/bits=*-12 2.46MB/s ± 0% 2.53MB/s ± 0% +3.18% (p=0.008 n=5+5) New/text=zero/size=10M/bits=*-12 2.35MB/s ± 0% 2.42MB/s ± 0% +2.98% (p=0.016 n=4+5) New/text=zero/size=50M/bits=*-12 2.12MB/s ± 0% 2.18MB/s ± 0% +3.02% (p=0.008 n=5+5) New/text=rand/size=100K/bits=*-12 6.98MB/s ± 0% 7.22MB/s ± 0% +3.38% (p=0.016 n=4+5) New/text=rand/size=500K/bits=*-12 5.53MB/s ± 0% 5.64MB/s ± 0% +1.92% (p=0.008 n=5+5) New/text=rand/size=1M/bits=*-12 4.62MB/s ± 1% 5.06MB/s ± 0% +9.61% (p=0.008 n=5+5) New/text=rand/size=5M/bits=*-12 3.09MB/s ± 0% 3.43MB/s ± 0% +10.94% (p=0.016 n=4+5) New/text=rand/size=10M/bits=*-12 2.68MB/s ± 0% 2.95MB/s ± 0% +10.39% (p=0.008 n=5+5) New/text=rand/size=50M/bits=*-12 1.92MB/s ± 0% 2.06MB/s ± 1% +7.41% (p=0.008 n=5+5) SaveRestore/bits=*-12 243MB/s ± 1% 259MB/s ± 0% +6.68% (p=0.000 n=9+10) name 64-bit alloc/op 32-bit alloc/op delta New/text=opticks/size=100K/bits=*-12 1.62MB ± 0% 0.81MB ± 0% -50.00% (p=0.000 n=5+4) New/text=opticks/size=500K/bits=*-12 8.07MB ± 0% 4.04MB ± 0% -49.89% (p=0.008 n=5+5) New/text=go/size=100K/bits=*-12 1.62MB ± 0% 0.81MB ± 0% -50.00% (p=0.008 n=5+5) New/text=go/size=500K/bits=*-12 8.07MB ± 0% 4.04MB ± 0% -49.89% (p=0.029 n=4+4) New/text=go/size=1M/bits=*-12 16.1MB ± 0% 8.1MB ± 0% -49.95% (p=0.008 n=5+5) New/text=go/size=5M/bits=*-12 80.3MB ± 0% 40.2MB ± 0% ~ (p=0.079 n=4+5) New/text=go/size=10M/bits=*-12 160MB ± 0% 80MB ± 0% -50.00% (p=0.008 n=5+5) New/text=go/size=50M/bits=*-12 805MB ± 0% 402MB ± 0% -50.06% (p=0.029 n=4+4) New/text=zero/size=100K/bits=*-12 3.02MB ± 0% 1.46MB ± 0% ~ (p=0.079 n=4+5) New/text=zero/size=500K/bits=*-12 19.7MB ± 0% 8.7MB ± 0% -55.98% (p=0.008 n=5+5) New/text=zero/size=1M/bits=*-12 39.0MB ± 0% 19.7MB ± 0% -49.60% (p=0.000 n=5+4) New/text=zero/size=5M/bits=*-12 169MB ± 0% 85MB ± 0% -49.46% (p=0.029 n=4+4) New/text=zero/size=10M/bits=*-12 333MB ± 0% 169MB ± 0% -49.43% (p=0.000 n=5+4) New/text=zero/size=50M/bits=*-12 1.63GB ± 0% 0.74GB ± 0% -54.61% (p=0.008 n=5+5) New/text=rand/size=100K/bits=*-12 1.61MB ± 0% 0.81MB ± 0% -50.00% (p=0.000 n=5+4) New/text=rand/size=500K/bits=*-12 8.07MB ± 0% 4.04MB ± 0% -49.89% (p=0.000 n=5+4) New/text=rand/size=1M/bits=*-12 16.1MB ± 0% 8.1MB ± 0% -49.95% (p=0.029 n=4+4) New/text=rand/size=5M/bits=*-12 80.7MB ± 0% 40.3MB ± 0% -50.06% (p=0.008 n=5+5) New/text=rand/size=10M/bits=*-12 161MB ± 0% 81MB ± 0% -50.03% (p=0.008 n=5+5) New/text=rand/size=50M/bits=*-12 806MB ± 0% 403MB ± 0% -50.00% (p=0.016 n=4+5) SaveRestore/bits=*-12 9.47MB ± 0% 5.28MB ± 0% -44.29% (p=0.000 n=9+8) https://perf.golang.org/search?q=upload:20190126.1+|+bits:64+vs+bits:32 Fixes #6816. Change-Id: Ied2fbea519a202ecc43719debcd233344ce38847 Reviewed-on: https://go-review.googlesource.com/c/go/+/174097 Run-TryBot: Russ Cox <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Brad Fitzpatrick <[email protected]>
1 parent b098c0f commit 45be353

File tree

5 files changed

+525
-84
lines changed

5 files changed

+525
-84
lines changed

src/index/suffixarray/gen64.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright 2019 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// +build ignore
6+
7+
// Gen64 generates qsufsort64.go from qsufsort.go by s/32/64/g.
8+
package main
9+
10+
import (
11+
"bytes"
12+
"io/ioutil"
13+
"log"
14+
)
15+
16+
func main() {
17+
log.SetPrefix("gen64: ")
18+
log.SetFlags(0)
19+
20+
data, err := ioutil.ReadFile("qsufsort.go")
21+
if err != nil {
22+
log.Fatal(err)
23+
}
24+
25+
data = bytes.Replace(data, []byte("\n\n"), []byte("\n\n// Code generated by gen64.go; DO NOT EDIT.\n//go:generate go run gen64.go\n\n"), 1)
26+
data = bytes.Replace(data, []byte("32"), []byte("64"), -1)
27+
28+
if err := ioutil.WriteFile("qsufsort64.go", data, 0666); err != nil {
29+
log.Fatal(err)
30+
}
31+
}

src/index/suffixarray/qsufsort.go

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -24,26 +24,28 @@
2424

2525
package suffixarray
2626

27-
import "sort"
27+
import (
28+
"sort"
29+
)
2830

29-
func qsufsort(data []byte) []int {
31+
func qsufsort32(data []byte) []int32 {
3032
// initial sorting by first byte of suffix
31-
sa := sortedByFirstByte(data)
33+
sa := sortedByFirstByte32(data)
3234
if len(sa) < 2 {
3335
return sa
3436
}
3537
// initialize the group lookup table
3638
// this becomes the inverse of the suffix array when all groups are sorted
37-
inv := initGroups(sa, data)
39+
inv := initGroups32(sa, data)
3840

3941
// the index starts 1-ordered
40-
sufSortable := &suffixSortable{sa: sa, inv: inv, h: 1}
42+
sufSortable := &suffixSortable32{sa: sa, inv: inv, h: 1}
4143

42-
for sa[0] > -len(sa) { // until all suffixes are one big sorted group
44+
for sa[0] > -int32(len(sa)) { // until all suffixes are one big sorted group
4345
// The suffixes are h-ordered, make them 2*h-ordered
44-
pi := 0 // pi is first position of first group
45-
sl := 0 // sl is negated length of sorted groups
46-
for pi < len(sa) {
46+
pi := int32(0) // pi is first position of first group
47+
sl := int32(0) // sl is negated length of sorted groups
48+
for pi < int32(len(sa)) {
4749
if s := sa[pi]; s < 0 { // if pi starts sorted group
4850
pi -= s // skip over sorted group
4951
sl += s // add negated length to sl
@@ -67,12 +69,12 @@ func qsufsort(data []byte) []int {
6769
}
6870

6971
for i := range sa { // reconstruct suffix array from inverse
70-
sa[inv[i]] = i
72+
sa[inv[i]] = int32(i)
7173
}
7274
return sa
7375
}
7476

75-
func sortedByFirstByte(data []byte) []int {
77+
func sortedByFirstByte32(data []byte) []int32 {
7678
// total byte counts
7779
var count [256]int
7880
for _, b := range data {
@@ -84,20 +86,20 @@ func sortedByFirstByte(data []byte) []int {
8486
count[b], sum = sum, count[b]+sum
8587
}
8688
// iterate through bytes, placing index into the correct spot in sa
87-
sa := make([]int, len(data))
89+
sa := make([]int32, len(data))
8890
for i, b := range data {
89-
sa[count[b]] = i
91+
sa[count[b]] = int32(i)
9092
count[b]++
9193
}
9294
return sa
9395
}
9496

95-
func initGroups(sa []int, data []byte) []int {
97+
func initGroups32(sa []int32, data []byte) []int32 {
9698
// label contiguous same-letter groups with the same group number
97-
inv := make([]int, len(data))
98-
prevGroup := len(sa) - 1
99+
inv := make([]int32, len(data))
100+
prevGroup := int32(len(sa)) - 1
99101
groupByte := data[sa[prevGroup]]
100-
for i := len(sa) - 1; i >= 0; i-- {
102+
for i := int32(len(sa)) - 1; i >= 0; i-- {
101103
if b := data[sa[i]]; b < groupByte {
102104
if prevGroup == i+1 {
103105
sa[i+1] = -1
@@ -114,13 +116,13 @@ func initGroups(sa []int, data []byte) []int {
114116
// This is necessary to ensure the suffix "a" is before "aba"
115117
// when using a potentially unstable sort.
116118
lastByte := data[len(data)-1]
117-
s := -1
119+
s := int32(-1)
118120
for i := range sa {
119121
if sa[i] >= 0 {
120122
if data[sa[i]] == lastByte && s == -1 {
121-
s = i
123+
s = int32(i)
122124
}
123-
if sa[i] == len(sa)-1 {
125+
if sa[i] == int32(len(sa))-1 {
124126
sa[i], sa[s] = sa[s], sa[i]
125127
inv[sa[s]] = s
126128
sa[s] = -1 // mark it as an isolated sorted group
@@ -131,31 +133,31 @@ func initGroups(sa []int, data []byte) []int {
131133
return inv
132134
}
133135

134-
type suffixSortable struct {
135-
sa []int
136-
inv []int
137-
h int
138-
buf []int // common scratch space
136+
type suffixSortable32 struct {
137+
sa []int32
138+
inv []int32
139+
h int32
140+
buf []int32 // common scratch space
139141
}
140142

141-
func (x *suffixSortable) Len() int { return len(x.sa) }
142-
func (x *suffixSortable) Less(i, j int) bool { return x.inv[x.sa[i]+x.h] < x.inv[x.sa[j]+x.h] }
143-
func (x *suffixSortable) Swap(i, j int) { x.sa[i], x.sa[j] = x.sa[j], x.sa[i] }
143+
func (x *suffixSortable32) Len() int { return len(x.sa) }
144+
func (x *suffixSortable32) Less(i, j int) bool { return x.inv[x.sa[i]+x.h] < x.inv[x.sa[j]+x.h] }
145+
func (x *suffixSortable32) Swap(i, j int) { x.sa[i], x.sa[j] = x.sa[j], x.sa[i] }
144146

145-
func (x *suffixSortable) updateGroups(offset int) {
147+
func (x *suffixSortable32) updateGroups(offset int32) {
146148
bounds := x.buf[0:0]
147149
group := x.inv[x.sa[0]+x.h]
148150
for i := 1; i < len(x.sa); i++ {
149151
if g := x.inv[x.sa[i]+x.h]; g > group {
150-
bounds = append(bounds, i)
152+
bounds = append(bounds, int32(i))
151153
group = g
152154
}
153155
}
154-
bounds = append(bounds, len(x.sa))
156+
bounds = append(bounds, int32(len(x.sa)))
155157
x.buf = bounds
156158

157159
// update the group numberings after all new groups are determined
158-
prev := 0
160+
prev := int32(0)
159161
for _, b := range bounds {
160162
for i := prev; i < b; i++ {
161163
x.inv[x.sa[i]] = offset + b - 1

src/index/suffixarray/qsufsort64.go

Lines changed: 173 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)