Commit 601045e8 authored by Marcel van Lohuizen's avatar Marcel van Lohuizen

exp/locale/collate: changed trie in first step towards support for multiple locales.

- Allow handles into the trie for different locales.  Multiple tables share the same
  try to allow for reuse of blocks.
- Significantly improved memory footprint and reduced allocations of trieNodes.
  This speeds up generation by about 30% and allows keeping trieNodes around
  for multiple locales during generation.
- Renamed print method to fprint.

R=r
CC=golang-dev
https://golang.org/cl/6408052
parent 48ca3f28
......@@ -66,6 +66,7 @@ func (e *entry) contractionStarter() bool {
// tables using Add and AddTailoring before making any call to Build. This allows
// Builder to ensure that a root table can support tailorings for each locale.
type Builder struct {
index *trieBuilder
entryMap map[string]*entry
entry []*entry
t *table
......@@ -76,6 +77,7 @@ type Builder struct {
// NewBuilder returns a new Builder.
func NewBuilder() *Builder {
b := &Builder{
index: newTrieBuilder(),
entryMap: make(map[string]*entry),
}
return b
......@@ -218,7 +220,7 @@ func (b *Builder) Print(w io.Writer) (int, error) {
return 0, err
}
// TODO: support multiple locales
n, _, err := t.print(w, "root")
n, _, err := t.fprint(w, "root")
return n, err
}
......@@ -510,7 +512,8 @@ func (b *Builder) buildTrie() {
t.insert(e.runes[0], ce)
}
}
i, err := t.generate()
b.t.root = b.index.addTrie(t)
i, err := b.index.generate()
b.t.index = *i
b.error(err)
}
......@@ -14,6 +14,7 @@ import (
// It implements the non-exported interface collate.tableInitializer
type table struct {
index trie // main trie
root *trieHandle
// expansion info
expandElem []uint32
......@@ -32,6 +33,10 @@ func (t *table) TrieValues() []uint32 {
return t.index.values
}
func (t *table) FirstBlockOffsets() (i, v uint16) {
return t.root.lookupStart, t.root.valueStart
}
func (t *table) ExpandElems() []uint32 {
return t.expandElem
}
......@@ -51,7 +56,7 @@ func (t *table) MaxContractLen() int {
// print writes the table as Go compilable code to w. It prefixes the
// variable names with name. It returns the number of bytes written
// and the size of the resulting table.
func (t *table) print(w io.Writer, name string) (n, size int, err error) {
func (t *table) fprint(w io.Writer, name string) (n, size int, err error) {
update := func(nn, sz int, e error) {
n += nn
if err == nil {
......@@ -66,7 +71,7 @@ func (t *table) print(w io.Writer, name string) (n, size int, err error) {
// Write main table.
size += int(reflect.TypeOf(*t).Size())
p("var %sTable = table{\n", name)
update(t.index.printStruct(w, name))
update(t.index.printStruct(w, t.root, name))
p(",\n")
p("%sExpandElem[:],\n", name)
update(t.contractTries.printStruct(w, name))
......
......@@ -15,7 +15,6 @@ import (
"fmt"
"hash/fnv"
"io"
"log"
"reflect"
)
......@@ -24,6 +23,11 @@ const (
blockOffset = 2 // Substract 2 blocks to compensate for the 0x80 added to continuation bytes.
)
type trieHandle struct {
lookupStart uint16 // offset in table for first byte
valueStart uint16 // offset in table for first byte
}
type trie struct {
index []uint16
values []uint32
......@@ -31,181 +35,189 @@ type trie struct {
// trieNode is the intermediate trie structure used for generating a trie.
type trieNode struct {
table [256]*trieNode
value int64
index []*trieNode
value []uint32
b byte
leaf bool
ref uint16
}
func newNode() *trieNode {
return new(trieNode)
return &trieNode{
index: make([]*trieNode, 64),
value: make([]uint32, 128), // root node size is 128 instead of 64
}
}
func (n *trieNode) isInternal() bool {
internal := true
for i := 0; i < 256; i++ {
if nn := n.table[i]; nn != nil {
if !internal && !nn.leaf {
log.Fatalf("trie:isInternal: node contains both leaf and non-leaf children (%v)", n)
}
internal = internal && !nn.leaf
}
}
return internal
return n.value != nil
}
func (n *trieNode) insert(r rune, value uint32) {
for _, b := range []byte(string(r)) {
if n.leaf {
log.Fatalf("trie:insert: node (%#v) should not be a leaf", n)
const maskx = 0x3F // mask out two most-significant bits
str := string(r)
if len(str) == 1 {
n.value[str[0]] = value
return
}
for i := 0; i < len(str)-1; i++ {
b := str[i] & maskx
if n.index == nil {
n.index = make([]*trieNode, blockSize)
}
nn := n.table[b]
nn := n.index[b]
if nn == nil {
nn = newNode()
nn = &trieNode{}
nn.b = b
n.table[b] = nn
n.index[b] = nn
}
n = nn
}
n.value = int64(value)
n.leaf = true
if n.value == nil {
n.value = make([]uint32, blockSize)
}
b := str[len(str)-1] & maskx
n.value[b] = value
}
type nodeIndex struct {
type trieBuilder struct {
t *trie
roots []*trieHandle
lookupBlocks []*trieNode
valueBlocks []*trieNode
lookupBlockIdx map[uint32]int64
valueBlockIdx map[uint32]int64
lookupBlockIdx map[uint32]*trieNode
valueBlockIdx map[uint32]*trieNode
}
func newIndex() *nodeIndex {
index := &nodeIndex{}
func newTrieBuilder() *trieBuilder {
index := &trieBuilder{}
index.lookupBlocks = make([]*trieNode, 0)
index.valueBlocks = make([]*trieNode, 0)
index.lookupBlockIdx = make(map[uint32]int64)
index.valueBlockIdx = make(map[uint32]int64)
index.lookupBlockIdx = make(map[uint32]*trieNode)
index.valueBlockIdx = make(map[uint32]*trieNode)
// The third nil is the default null block. The other two blocks
// are used to guarantee an offset of at least 3 for each block.
index.lookupBlocks = append(index.lookupBlocks, nil, nil, nil)
index.t = &trie{}
return index
}
func computeOffsets(index *nodeIndex, n *trieNode) int64 {
if n.leaf {
return n.value
}
func (b *trieBuilder) computeOffsets(n *trieNode) *trieNode {
hasher := fnv.New32()
// We only index continuation bytes.
for i := 0; i < blockSize; i++ {
v := int64(0)
if nn := n.table[0x80+i]; nn != nil {
v = computeOffsets(index, nn)
if n.index != nil {
for i, nn := range n.index {
v := uint16(0)
if nn != nil {
nn = b.computeOffsets(nn)
n.index[i] = nn
v = nn.ref
}
hasher.Write([]byte{byte(v >> 8), byte(v)})
}
hasher.Write([]byte{byte(v >> 24), byte(v >> 16), byte(v >> 8), byte(v)})
}
h := hasher.Sum32()
if n.isInternal() {
v, ok := index.lookupBlockIdx[h]
h := hasher.Sum32()
nn, ok := b.lookupBlockIdx[h]
if !ok {
v = int64(len(index.lookupBlocks)) - blockOffset
index.lookupBlocks = append(index.lookupBlocks, n)
index.lookupBlockIdx[h] = v
n.ref = uint16(len(b.lookupBlocks)) - blockOffset
b.lookupBlocks = append(b.lookupBlocks, n)
b.lookupBlockIdx[h] = n
} else {
n = nn
}
n.value = v
} else {
v, ok := index.valueBlockIdx[h]
for _, v := range n.value {
hasher.Write([]byte{byte(v >> 24), byte(v >> 16), byte(v >> 8), byte(v)})
}
h := hasher.Sum32()
nn, ok := b.valueBlockIdx[h]
if !ok {
v = int64(len(index.valueBlocks)) - blockOffset
index.valueBlocks = append(index.valueBlocks, n)
index.valueBlockIdx[h] = v
n.ref = uint16(len(b.valueBlocks)) - blockOffset
b.valueBlocks = append(b.valueBlocks, n)
b.valueBlockIdx[h] = n
} else {
n = nn
}
n.value = v
}
return n.value
return n
}
func genValueBlock(t *trie, n *trieNode, offset int) error {
for i := 0; i < blockSize; i++ {
v := int64(0)
if nn := n.table[i+offset]; nn != nil {
v = nn.value
}
if v >= 1<<32 {
return fmt.Errorf("value %d at index %d does not fit in uint32", v, len(t.values))
}
t.values = append(t.values, uint32(v))
func (b *trieBuilder) addStartValueBlock(n *trieNode) uint16 {
hasher := fnv.New32()
for _, v := range n.value[:2*blockSize] {
hasher.Write([]byte{byte(v >> 24), byte(v >> 16), byte(v >> 8), byte(v)})
}
return nil
h := hasher.Sum32()
nn, ok := b.valueBlockIdx[h]
if !ok {
n.ref = uint16(len(b.valueBlocks))
b.valueBlocks = append(b.valueBlocks, n)
// Add a dummy block to accommodate the double block size.
b.valueBlocks = append(b.valueBlocks, nil)
b.valueBlockIdx[h] = n
} else {
n = nn
}
return n.ref
}
func genLookupBlock(t *trie, n *trieNode, offset int) error {
for i := 0; i < blockSize; i++ {
v := int64(0)
if nn := n.table[i+offset]; nn != nil {
v = nn.value
}
if v >= 1<<16 {
return fmt.Errorf("value %d at index %d does not fit in uint16", v, len(t.index))
func genValueBlock(t *trie, n *trieNode) {
if n != nil {
for _, v := range n.value {
t.values = append(t.values, v)
}
t.index = append(t.index, uint16(v))
}
return nil
}
// generate generates and returns the trie for n.
func (n *trieNode) generate() (t *trie, err error) {
seterr := func(e error) {
if err == nil {
err = e
func genLookupBlock(t *trie, n *trieNode) {
for _, nn := range n.index {
v := uint16(0)
if nn != nil {
v = nn.ref
}
t.index = append(t.index, v)
}
index := newIndex()
// Values for 7-bit ASCII are stored in the first of two blocks, followed by a nil block.
index.valueBlocks = append(index.valueBlocks, nil, nil, nil)
// First byte of multi-byte UTF-8 codepoints are indexed in 4th block.
index.lookupBlocks = append(index.lookupBlocks, nil, nil, nil, nil)
// Index starter bytes of multi-byte UTF-8.
for i := 0xC0; i < 0x100; i++ {
if n.table[i] != nil {
computeOffsets(index, n.table[i])
}
}
func (b *trieBuilder) addTrie(n *trieNode) *trieHandle {
h := &trieHandle{}
b.roots = append(b.roots, h)
h.valueStart = b.addStartValueBlock(n)
if len(b.roots) == 1 {
// We insert a null block after the first start value block.
// This ensures that continuation bytes UTF-8 sequences of length
// greater than 2 will automatically hit a null block if there
// was an undefined entry.
b.valueBlocks = append(b.valueBlocks, nil)
}
t = &trie{}
seterr(genValueBlock(t, n, 0))
seterr(genValueBlock(t, n, 64))
seterr(genValueBlock(t, newNode(), 0))
for i := 3; i < len(index.valueBlocks); i++ {
seterr(genValueBlock(t, index.valueBlocks[i], 0x80))
n = b.computeOffsets(n)
// Offset by one extra block as the first byte starts at 0xC0 instead of 0x80.
h.lookupStart = n.ref - 1
return h
}
// generate generates and returns the trie for n.
func (b *trieBuilder) generate() (t *trie, err error) {
t = b.t
if len(b.valueBlocks) >= 1<<16 {
return nil, fmt.Errorf("maximum number of value blocks exceeded (%d > %d)", len(b.valueBlocks), 1<<16)
}
if len(index.valueBlocks) >= 1<<16 {
seterr(fmt.Errorf("maximum number of value blocks exceeded (%d > %d)", len(index.valueBlocks), 1<<16))
return
if len(b.lookupBlocks) >= 1<<16 {
return nil, fmt.Errorf("maximum number of lookup blocks exceeded (%d > %d)", len(b.lookupBlocks), 1<<16)
}
seterr(genLookupBlock(t, newNode(), 0))
seterr(genLookupBlock(t, newNode(), 0))
seterr(genLookupBlock(t, newNode(), 0))
seterr(genLookupBlock(t, n, 0xC0))
for i := 4; i < len(index.lookupBlocks); i++ {
seterr(genLookupBlock(t, index.lookupBlocks[i], 0x80))
genValueBlock(t, b.valueBlocks[0])
genValueBlock(t, &trieNode{value: make([]uint32, 64)})
for i := 2; i < len(b.valueBlocks); i++ {
genValueBlock(t, b.valueBlocks[i])
}
return
}
// print writes a compilable trie to w. It returns the number of characters
// printed and the size of the generated structure in bytes.
func (t *trie) print(w io.Writer, name string) (n, size int, err error) {
update3 := func(nn, sz int, e error) {
n += nn
if err == nil {
err = e
}
size += sz
n := &trieNode{index: make([]*trieNode, 64)}
genLookupBlock(t, n)
genLookupBlock(t, n)
genLookupBlock(t, n)
for i := 3; i < len(b.lookupBlocks); i++ {
genLookupBlock(t, b.lookupBlocks[i])
}
update2 := func(nn int, e error) { update3(nn, 0, e) }
update3(t.printArrays(w, name))
update2(fmt.Fprintf(w, "var %sTrie = ", name))
update3(t.printStruct(w, name))
update2(fmt.Fprintln(w))
return
return b.t, nil
}
func (t *trie) printArrays(w io.Writer, name string) (n, size int, err error) {
......@@ -261,8 +273,9 @@ func (t *trie) printArrays(w io.Writer, name string) (n, size int, err error) {
return n, nv*4 + ni*2, err
}
func (t *trie) printStruct(w io.Writer, name string) (n, sz int, err error) {
n, err = fmt.Fprintf(w, "trie{ %sLookup[:], %sValues[:]}", name, name)
func (t *trie) printStruct(w io.Writer, handle *trieHandle, name string) (n, sz int, err error) {
const msg = "trie{ %sLookup[%d:], %sValues[%d:], %sLookup[:], %sValues[:]}"
n, err = fmt.Fprintf(w, msg, name, handle.lookupStart*blockSize, name, handle.valueStart*blockSize, name, name)
sz += int(reflect.TypeOf(trie{}).Size())
return
}
......@@ -6,6 +6,7 @@ package build
import (
"bytes"
"fmt"
"testing"
)
......@@ -24,7 +25,9 @@ func makeTestTrie(t *testing.T) trie {
for i, r := range testRunes {
n.insert(r, uint32(i))
}
tr, err := n.generate()
idx := newTrieBuilder()
idx.addTrie(n)
tr, err := idx.generate()
if err != nil {
t.Errorf(err.Error())
}
......@@ -34,9 +37,11 @@ func makeTestTrie(t *testing.T) trie {
func TestGenerateTrie(t *testing.T) {
testdata := makeTestTrie(t)
buf := &bytes.Buffer{}
testdata.print(buf, "test")
testdata.printArrays(buf, "test")
fmt.Fprintf(buf, "var testTrie = ")
testdata.printStruct(buf, &trieHandle{19, 0}, "test")
if output != buf.String() {
t.Errorf("output differs")
t.Error("output differs")
}
}
......@@ -79,25 +84,24 @@ var testLookup = [640]uint16 {
// Block 0x1, offset 0x40
// Block 0x2, offset 0x80
// Block 0x3, offset 0xc0
0x0c2:0x01, 0x0c4:0x02,
0x0c8:0x03,
0x0df:0x04,
0x0e0:0x02,
0x0ef:0x03,
0x0f0:0x05, 0x0f4:0x07,
0x0e0:0x05, 0x0e6:0x06,
// Block 0x4, offset 0x100
0x120:0x05, 0x126:0x06,
0x13f:0x07,
// Block 0x5, offset 0x140
0x17f:0x07,
0x140:0x08, 0x144:0x09,
// Block 0x6, offset 0x180
0x180:0x08, 0x184:0x09,
0x190:0x03,
// Block 0x7, offset 0x1c0
0x1d0:0x04,
0x1ff:0x0a,
// Block 0x8, offset 0x200
0x23f:0x0a,
0x20f:0x05,
// Block 0x9, offset 0x240
0x24f:0x06,
0x242:0x01, 0x244:0x02,
0x248:0x03,
0x25f:0x04,
0x260:0x01,
0x26f:0x02,
0x270:0x04, 0x274:0x06,
}
var testTrie = trie{ testLookup[:], testValues[:]}
`
var testTrie = trie{ testLookup[1216:], testValues[0:], testLookup[:], testValues[:]}`
......@@ -12,8 +12,11 @@ func Init(data interface{}) *Collator {
return nil
}
t := &table{}
loff, voff := init.FirstBlockOffsets()
t.index.index = init.TrieIndex()
t.index.index0 = t.index.index[blockSize*loff:]
t.index.values = init.TrieValues()
t.index.values0 = t.index.values[blockSize*voff:]
t.expandElem = init.ExpandElems()
t.contractTries = init.ContractTries()
t.contractElem = init.ContractElems()
......@@ -24,6 +27,7 @@ func Init(data interface{}) *Collator {
type tableInitializer interface {
TrieIndex() []uint16
TrieValues() []uint32
FirstBlockOffsets() (lookup, value uint16)
ExpandElems() []uint32
ContractTries() []struct{ l, h, n, i uint8 }
ContractElems() []uint32
......
......@@ -45,9 +45,10 @@ func makeTable(in []input) (*collate.Collator, error) {
b.Add([]rune(r.str), r.ces)
}
c, err := b.Build("")
if err == nil {
collate.InitCollator(c)
if c == nil {
return nil, err
}
collate.InitCollator(c)
return c, err
}
......
This diff is collapsed.
......@@ -14,8 +14,10 @@ package collate
const blockSize = 64
type trie struct {
index []uint16
values []uint32
index0 []uint16 // index for first byte (0xC0-0xFF)
values0 []uint32 // index for first byte (0x00-0x7F)
index []uint16
values []uint32
}
const (
......@@ -40,14 +42,14 @@ func (t *trie) lookup(s []byte) (v colElem, sz int) {
c0 := s[0]
switch {
case c0 < tx:
return colElem(t.values[c0]), 1
return colElem(t.values0[c0]), 1
case c0 < t2:
return 0, 1
case c0 < t3:
if len(s) < 2 {
return 0, 0
}
i := t.index[c0]
i := t.index0[c0]
c1 := s[1]
if c1 < tx || t2 <= c1 {
return 0, 1
......@@ -57,7 +59,7 @@ func (t *trie) lookup(s []byte) (v colElem, sz int) {
if len(s) < 3 {
return 0, 0
}
i := t.index[c0]
i := t.index0[c0]
c1 := s[1]
if c1 < tx || t2 <= c1 {
return 0, 1
......@@ -73,7 +75,7 @@ func (t *trie) lookup(s []byte) (v colElem, sz int) {
if len(s) < 4 {
return 0, 0
}
i := t.index[c0]
i := t.index0[c0]
c1 := s[1]
if c1 < tx || t2 <= c1 {
return 0, 1
......
......@@ -89,18 +89,18 @@ var testValues = [832]uint32{
}
var testLookup = [640]uint16{
0x0c2: 0x01, 0x0c4: 0x02,
0x0c8: 0x03,
0x0df: 0x04,
0x0e0: 0x02,
0x0ef: 0x03,
0x0f0: 0x05, 0x0f4: 0x07,
0x120: 0x05, 0x126: 0x06,
0x17f: 0x07,
0x180: 0x08, 0x184: 0x09,
0x1d0: 0x04,
0x23f: 0x0a,
0x24f: 0x06,
0x0e0: 0x05, 0x0e6: 0x06,
0x13f: 0x07,
0x140: 0x08, 0x144: 0x09,
0x190: 0x03,
0x1ff: 0x0a,
0x20f: 0x05,
0x242: 0x01, 0x244: 0x02,
0x248: 0x03,
0x25f: 0x04,
0x260: 0x01,
0x26f: 0x02,
0x270: 0x04, 0x274: 0x06,
}
var testTrie = trie{testLookup[:], testValues[:]}
var testTrie = trie{testLookup[6*blockSize:], testValues[:], testLookup[:], testValues[:]}
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