hugolib/doctree: Simplify lock setup in SimpleThreadSafeTree to reduce read allocation

```
                                      │    sec/op    │   sec/op     vs base               │
SimpleThreadSafeTree/Get-10             45.80n ± 11%   27.71n ± 7%  -39.51% (p=0.002 n=6)
SimpleThreadSafeTree/Insert-10          40.96n ±  1%   40.36n ± 0%   -1.47% (p=0.002 n=6)
SimpleThreadSafeTree/LongestPrefix-10   47.12n ±  0%   32.60n ± 1%  -30.81% (p=0.002 n=6)
geomean                                 44.54n         33.16n       -25.57%

                                      │ cmp152.bench │       perf-allloc20251118.bench        │
                                      │     B/op     │    B/op     vs base                    │
SimpleThreadSafeTree/Get-10               16.00 ± 0%    0.00 ± 0%  -100.00% (p=0.002 n=6)
SimpleThreadSafeTree/Insert-10            8.000 ± 0%   8.000 ± 0%         ~ (p=1.000 n=6) ¹
SimpleThreadSafeTree/LongestPrefix-10     16.00 ± 0%    0.00 ± 0%  -100.00% (p=0.002 n=6)
````
This commit is contained in:
Bjørn Erik Pedersen
2025-11-18 12:30:50 +01:00
parent 56d7925a97
commit fee095768c
2 changed files with 62 additions and 32 deletions
+9 -32
View File
@@ -127,33 +127,16 @@ func NewSimpleThreadSafeTree[T any]() *SimpleThreadSafeTree[T] {
// SimpleThreadSafeTree is a thread safe radix tree that holds T.
type SimpleThreadSafeTree[T any] struct {
mu *sync.RWMutex
noLock bool
tree *radix.Tree
zero T
mu *sync.RWMutex
tree *radix.Tree
zero T
}
var noopFunc = func() {}
func (tree *SimpleThreadSafeTree[T]) readLock() func() {
if tree.noLock {
return noopFunc
}
tree.mu.RLock()
return tree.mu.RUnlock
}
func (tree *SimpleThreadSafeTree[T]) writeLock() func() {
if tree.noLock {
return noopFunc
}
tree.mu.Lock()
return tree.mu.Unlock
}
func (tree *SimpleThreadSafeTree[T]) Get(s string) T {
unlock := tree.readLock()
defer unlock()
tree.mu.RLock()
defer tree.mu.RUnlock()
if v, ok := tree.tree.Get(s); ok {
return v.(T)
@@ -162,8 +145,8 @@ func (tree *SimpleThreadSafeTree[T]) Get(s string) T {
}
func (tree *SimpleThreadSafeTree[T]) LongestPrefix(s string) (string, T) {
unlock := tree.readLock()
defer unlock()
tree.mu.RLock()
defer tree.mu.RUnlock()
if s, v, ok := tree.tree.LongestPrefix(s); ok {
return s, v.(T)
@@ -172,8 +155,8 @@ func (tree *SimpleThreadSafeTree[T]) LongestPrefix(s string) (string, T) {
}
func (tree *SimpleThreadSafeTree[T]) Insert(s string, v T) T {
unlock := tree.writeLock()
defer unlock()
tree.mu.Lock()
defer tree.mu.Unlock()
tree.tree.Insert(s, v)
return v
@@ -193,12 +176,6 @@ func (tree *SimpleThreadSafeTree[T]) Lock(lockType LockType) func() {
return noopFunc
}
func (tree SimpleThreadSafeTree[T]) LockTree(lockType LockType) (TreeThreadSafe[T], func()) {
unlock := tree.Lock(lockType)
tree.noLock = true
return &tree, unlock // create a copy of tree with the noLock flag set to true.
}
func (tree *SimpleThreadSafeTree[T]) WalkPrefix(lockType LockType, s string, f func(s string, v T) (bool, error)) error {
commit := tree.Lock(lockType)
defer commit()
+53
View File
@@ -0,0 +1,53 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
//
// 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 doctree
import (
"fmt"
"testing"
)
func BenchmarkSimpleThreadSafeTree(b *testing.B) {
newTestTree := func() TreeThreadSafe[int] {
t := NewSimpleThreadSafeTree[int]()
for i := 0; i < 1000; i++ {
t.Insert(fmt.Sprintf("key%d", i), i)
}
return t
}
b.Run("Get", func(b *testing.B) {
t := newTestTree()
b.ResetTimer()
for b.Loop() {
t.Get("key500")
}
})
b.Run("Insert", func(b *testing.B) {
t := newTestTree()
b.ResetTimer()
for b.Loop() {
t.Insert("key500", 501)
}
})
b.Run("LongestPrefix", func(b *testing.B) {
t := newTestTree()
b.ResetTimer()
for b.Loop() {
t.LongestPrefix("key500extra")
}
})
}