Skip to content

Commit

Permalink
Add node48 to stree
Browse files Browse the repository at this point in the history
A `node256` is nearly 4KB in memory whereas a `node48` is closer to 1KB.

Signed-off-by: Neil Twigg <neil@nats.io>
  • Loading branch information
neilalexander committed Jun 24, 2024
1 parent 3a32c41 commit d2bede5
Show file tree
Hide file tree
Showing 5 changed files with 220 additions and 7 deletions.
1 change: 1 addition & 0 deletions server/stree/dump.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ func (t *SubjectTree[T]) dump(w io.Writer, n node, depth int) {
func (n *leaf[T]) kind() string { return "LEAF" }
func (n *node4) kind() string { return "NODE4" }
func (n *node16) kind() string { return "NODE16" }
func (n *node48) kind() string { return "NODE48" }
func (n *node256) kind() string { return "NODE256" }

// Calculates the indendation, etc.
Expand Down
2 changes: 1 addition & 1 deletion server/stree/node16.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func (n *node16) findChild(c byte) *node {
func (n *node16) isFull() bool { return n.size >= 16 }

func (n *node16) grow() node {
nn := newNode256(n.prefix)
nn := newNode48(n.prefix)
for i := 0; i < 16; i++ {
nn.addChild(n.key[i], n.child[i])
}
Expand Down
4 changes: 2 additions & 2 deletions server/stree/node256.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ func (n *node256) deleteChild(c byte) {

// Shrink if needed and return new node, otherwise return nil.
func (n *node256) shrink() node {
if n.size > 16 {
if n.size > 48 {
return nil
}
nn := newNode16(nil)
nn := newNode48(nil)
for c, child := range n.child {
if child != nil {
nn.addChild(byte(c), n.child[c])
Expand Down
110 changes: 110 additions & 0 deletions server/stree/node48.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Copyright 2023-2024 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package stree

// Node with 48 children
// Memory saving vs node256 comes from the fact that the child array is 16 bytes
// per `node` entry, so node256's 256*16=4096 vs node48's 256+(48*16)=1024
// Note that `key` is effectively 1-indexed, as 0 means no entry, so offset by 1
// Order of struct fields for best memory alignment (as per govet/fieldalignment)
type node48 struct {
key [256]byte
meta
child [48]node
}

func newNode48(prefix []byte) *node48 {
nn := &node48{}
nn.setPrefix(prefix)
return nn
}

func (n *node48) addChild(c byte, nn node) {
if n.size >= 48 {
panic("node48 full!")
}
n.child[n.size] = nn
n.key[c] = byte(n.size + 1) // 1-indexed
n.size++
}

func (n *node48) findChild(c byte) *node {
i := n.key[c]
if i == 0 {
return nil
}
return &n.child[i-1]
}

func (n *node48) isFull() bool { return n.size >= 48 }

func (n *node48) grow() node {
nn := newNode256(n.prefix)
for c := byte(0); c < 255; c++ {
if i := n.key[c]; i > 0 {
nn.addChild(c, n.child[i-1])
}
}
return nn
}

// Deletes a child from the node.
func (n *node48) deleteChild(c byte) {
i := n.key[c]
if i == 0 {
return
}
i-- // Adjust for 1-indexing
last := byte(n.size - 1)
if i < last {
n.child[i] = n.child[last]
for c := byte(0); c <= 255; c++ {
if n.key[c] == last+1 {
n.key[c] = i + 1
break
}
}
}
n.child[last] = nil
n.key[c] = 0
n.size--
}

// Shrink if needed and return new node, otherwise return nil.
func (n *node48) shrink() node {
if n.size > 16 {
return nil
}
nn := newNode16(nil)
for c := byte(0); c < 255; c++ {
if i := n.key[c]; i > 0 {
nn.addChild(c, n.child[i-1])
}
}
return nn
}

// Iterate over all children calling func f.
func (n *node48) iter(f func(node) bool) {
for _, c := range n.child {
if c != nil && !f(c) {
return
}
}
}

// Return our children as a slice.
func (n *node48) children() []node {
return n.child[:n.size]
}
110 changes: 106 additions & 4 deletions server/stree/stree_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ func TestSubjectTreeNodeGrow(t *testing.T) {
require_False(t, updated)
_, ok = st.root.(*node16)
require_True(t, ok)
// We do not have node48, so once we fill this we should jump to node256.
for i := 5; i < 16; i++ {
subj := b(fmt.Sprintf("foo.bar.%c", 'A'+i))
old, updated := st.Insert(subj, 22)
Expand All @@ -89,6 +88,20 @@ func TestSubjectTreeNodeGrow(t *testing.T) {
old, updated = st.Insert(b("foo.bar.Q"), 22)
require_True(t, old == nil)
require_False(t, updated)
_, ok = st.root.(*node48)
require_True(t, ok)
// Fill the node48.
for i := 17; i < 48; i++ {
subj := b(fmt.Sprintf("foo.bar.%c", 'A'+i))
old, updated := st.Insert(subj, 22)
require_True(t, old == nil)
require_False(t, updated)
}
// This one will trigger us to grow.
subj := b(fmt.Sprintf("foo.bar.%c", 'A'+49))
old, updated = st.Insert(subj, 22)
require_True(t, old == nil)
require_False(t, updated)
_, ok = st.root.(*node256)
require_True(t, ok)
}
Expand Down Expand Up @@ -160,13 +173,13 @@ func TestSubjectTreeNodeDelete(t *testing.T) {
require_Equal(t, *v, 22)
_, ok = st.root.(*node4)
require_True(t, ok)
// Now pop up to node256
// Now pop up to node48
st = NewSubjectTree[int]()
for i := 0; i < 17; i++ {
subj := fmt.Sprintf("foo.bar.%c", 'A'+i)
st.Insert(b(subj), 22)
}
_, ok = st.root.(*node256)
_, ok = st.root.(*node48)
require_True(t, ok)
v, found = st.Delete(b("foo.bar.A"))
require_True(t, found)
Expand All @@ -176,6 +189,22 @@ func TestSubjectTreeNodeDelete(t *testing.T) {
v, found = st.Find(b("foo.bar.B"))
require_True(t, found)
require_Equal(t, *v, 22)
// Now pop up to node256
st = NewSubjectTree[int]()
for i := 0; i < 49; i++ {
subj := fmt.Sprintf("foo.bar.%c", 'A'+i)
st.Insert(b(subj), 22)
}
_, ok = st.root.(*node256)
require_True(t, ok)
v, found = st.Delete(b("foo.bar.A"))
require_True(t, found)
require_Equal(t, *v, 22)
_, ok = st.root.(*node48)
require_True(t, ok)
v, found = st.Find(b("foo.bar.B"))
require_True(t, found)
require_Equal(t, *v, 22)
}

func TestSubjectTreeNodesAndPaths(t *testing.T) {
Expand Down Expand Up @@ -341,7 +370,7 @@ func TestSubjectTreeNoPrefix(t *testing.T) {
require_True(t, old == nil)
require_False(t, updated)
}
n, ok := st.root.(*node256)
n, ok := st.root.(*node48)
require_True(t, ok)
require_Equal(t, n.numChildren(), 26)
v, found := st.Delete(b("B"))
Expand Down Expand Up @@ -636,3 +665,76 @@ func TestSubjectTreeMatchAllPerf(t *testing.T) {
t.Logf("Match %q took %s and matched %d entries", f, time.Since(start), count)
}
}

func TestSubjectTreeNode48(t *testing.T) {
var a, b, c leaf[int]
var n node48

n.addChild('A', &a)
require_Equal(t, n.key['A'], 1)
require_True(t, n.child[0] != nil)
require_Equal(t, n.child[0].(*leaf[int]), &a)
require_Equal(t, len(n.children()), 1)

child := n.findChild('A')
require_True(t, child != nil)
require_Equal(t, (*child).(*leaf[int]), &a)

n.addChild('B', &b)
require_Equal(t, n.key['B'], 2)
require_True(t, n.child[1] != nil)
require_Equal(t, n.child[1].(*leaf[int]), &b)
require_Equal(t, len(n.children()), 2)

child = n.findChild('B')
require_True(t, child != nil)
require_Equal(t, (*child).(*leaf[int]), &b)

n.addChild('C', &c)
require_Equal(t, n.key['C'], 3)
require_True(t, n.child[2] != nil)
require_Equal(t, n.child[2].(*leaf[int]), &c)
require_Equal(t, len(n.children()), 3)

child = n.findChild('C')
require_True(t, child != nil)
require_Equal(t, (*child).(*leaf[int]), &c)

n.deleteChild('A')
require_Equal(t, len(n.children()), 2)
require_Equal(t, n.key['A'], 0) // Now deleted
require_Equal(t, n.key['B'], 2) // Untouched
require_Equal(t, n.key['C'], 1) // Where A was

child = n.findChild('A')
require_Equal(t, child, nil)
require_True(t, n.child[0] != nil)
require_Equal(t, n.child[0].(*leaf[int]), &c)

child = n.findChild('B')
require_True(t, child != nil)
require_Equal(t, (*child).(*leaf[int]), &b)
require_True(t, n.child[1] != nil)
require_Equal(t, n.child[1].(*leaf[int]), &b)

child = n.findChild('C')
require_True(t, child != nil)
require_Equal(t, (*child).(*leaf[int]), &c)
require_True(t, n.child[2] == nil)

var gotB, gotC bool
var iterations int
n.iter(func(n node) bool {
iterations++
if gb, ok := n.(*leaf[int]); ok && &b == gb {
gotB = true
}
if gc, ok := n.(*leaf[int]); ok && &c == gc {
gotC = true
}
return true
})
require_Equal(t, iterations, 2)
require_True(t, gotB)
require_True(t, gotC)
}

0 comments on commit d2bede5

Please sign in to comment.