golang泛型在btree的应用与优化
近几个月更新的 golang1.18 支持了泛型,正好前两天看到了 google 官方的 golang-btree 更新了 v2,使用泛型generics
代替了interface{}
,号称提升了 40% 的性能
Show me the code
https://github.com/google/btree
重新整理后的函数级别的完整的diff (原始MR点这里):
展开函数层面的完整diff
@@ -1,4 +1,4 @@ | |
1 | -
// Copyright 2014 Google Inc. |
2 |
// |
3 |
// Licensed under the Apache License, Version 2.0 (the "License"); |
4 |
// you may not use this file except in compliance with the License. |
@@ -12,8 +12,13 @@ | |
12 |
// See the License for the specific language governing permissions and |
13 |
// limitations under the License. |
14 | |
15 | -
//go:build |
16 | -
// +build |
17 | |
18 |
// Package btree implements in-memory B-Trees of arbitrary degree. |
19 |
// |
@@ -48,6 +53,11 @@ | |
48 |
// Its functions, therefore, exactly mirror those of |
49 |
// llrb.LLRB where possible. Unlike gollrb, though, we currently don't |
50 |
// support storing multiple equivalent values. |
51 |
package btree |
52 | |
53 |
import ( |
@@ -72,32 +82,27 @@ | |
72 |
DefaultFreeListSize = 32 |
73 |
) |
74 | |
75 | -
|
76 | -
nilItems = make(items, 16) |
77 | -
nilChildren = make(children, 16) |
78 | -
) |
79 | -
|
80 | -
// FreeList represents a free list of btree nodes. By default each |
81 |
// BTree has its own FreeList, but multiple BTrees can share the same |
82 | -
// FreeList. |
83 |
// Two Btrees using the same freelist are safe for concurrent write access. |
84 | -
type |
85 |
mu sync.Mutex |
86 | -
freelist []*node |
87 |
} |
88 | |
89 | -
// |
90 |
// size is the maximum size of the returned free list. |
91 | -
func |
92 | -
return & |
93 |
} |
94 | |
95 | -
func (f * |
96 |
f.mu.Lock() |
97 |
index := len(f.freelist) - 1 |
98 |
if index < 0 { |
99 |
f.mu.Unlock() |
100 | -
return new(node) |
101 |
} |
102 |
n = f.freelist[index] |
103 |
f.freelist[index] = nil |
@@ -106,9 +111,7 @@ | |
106 |
return |
107 |
} |
108 | |
109 | -
|
110 | -
// and false if it was discarded. |
111 | -
func (f *FreeList) freeNode(n *node) (out bool) { |
112 |
f.mu.Lock() |
113 |
if len(f.freelist) < cap(f.freelist) { |
114 |
f.freelist = append(f.freelist, n) |
@@ -118,37 +121,55 @@ | |
118 |
return |
119 |
} |
120 | |
121 | -
// |
122 |
// the tree. When this function returns false, iteration will stop and the |
123 |
// associated Ascend* function will immediately return. |
124 | -
type |
125 | |
126 | -
// |
127 |
// |
128 | -
// |
129 |
// and 2-4 children). |
130 | -
|
131 | -
|
132 |
} |
133 | |
134 | -
// |
135 | -
func |
136 |
if degree <= 1 { |
137 |
panic("bad degree") |
138 |
} |
139 | -
return & |
140 |
degree: degree, |
141 | -
cow: ©OnWriteContext{freelist: f}, |
142 |
} |
143 |
} |
144 | |
145 |
// items stores items in a node. |
146 | -
type items [] |
147 | |
148 |
// insertAt inserts a value into the given index, pushing all subsequent values |
149 |
// forward. |
150 | -
func (s *items) insertAt(index int, item |
151 | -
|
152 |
if index < len(*s) { |
153 |
copy((*s)[index+1:], (*s)[index:]) |
154 |
} |
@@ -157,100 +178,61 @@ | |
157 | |
158 |
// removeAt removes a value at a given index, pulling all subsequent values |
159 |
// back. |
160 | -
func (s *items) removeAt(index int) |
161 |
item := (*s)[index] |
162 |
copy((*s)[index:], (*s)[index+1:]) |
163 | -
|
164 |
*s = (*s)[:len(*s)-1] |
165 |
return item |
166 |
} |
167 | |
168 |
// pop removes and returns the last element in the list. |
169 | -
func (s *items) pop() (out |
170 |
index := len(*s) - 1 |
171 |
out = (*s)[index] |
172 | -
|
173 |
*s = (*s)[:index] |
174 |
return |
175 |
} |
176 | |
177 |
// truncate truncates this instance at index so that it contains only the |
178 |
// first index items. index must be less than or equal to length. |
179 | -
func (s *items) truncate(index int) { |
180 | -
var toClear items |
181 |
*s, toClear = (*s)[:index], (*s)[index:] |
182 | -
|
183 | -
|
184 |
} |
185 |
} |
186 | |
187 |
// find returns the index where the given item should be inserted into this |
188 |
// list. 'found' is true if the item already exists in the list at the given |
189 |
// index. |
190 | -
func (s items) find(item |
191 |
i := sort.Search(len(s), func(i int) bool { |
192 | -
return item |
193 |
}) |
194 | -
if i > 0 && !s[i-1] |
195 |
return i - 1, true |
196 |
} |
197 |
return i, false |
198 |
} |
199 | |
200 | -
// children stores child nodes in a node. |
201 | -
type children []*node |
202 | -
|
203 | -
// insertAt inserts a value into the given index, pushing all subsequent values |
204 | -
// forward. |
205 | -
func (s *children) insertAt(index int, n *node) { |
206 | -
*s = append(*s, nil) |
207 | -
if index < len(*s) { |
208 | -
copy((*s)[index+1:], (*s)[index:]) |
209 | -
} |
210 | -
(*s)[index] = n |
211 | -
} |
212 | -
|
213 | -
// removeAt removes a value at a given index, pulling all subsequent values |
214 | -
// back. |
215 | -
func (s *children) removeAt(index int) *node { |
216 | -
n := (*s)[index] |
217 | -
copy((*s)[index:], (*s)[index+1:]) |
218 | -
(*s)[len(*s)-1] = nil |
219 | -
*s = (*s)[:len(*s)-1] |
220 | -
return n |
221 | -
} |
222 | -
|
223 | -
// pop removes and returns the last element in the list. |
224 | -
func (s *children) pop() (out *node) { |
225 | -
index := len(*s) - 1 |
226 | -
out = (*s)[index] |
227 | -
(*s)[index] = nil |
228 | -
*s = (*s)[:index] |
229 | -
return |
230 | -
} |
231 | -
|
232 | -
// truncate truncates this instance at index so that it contains only the |
233 | -
// first index children. index must be less than or equal to length. |
234 | -
func (s *children) truncate(index int) { |
235 | -
var toClear children |
236 | -
*s, toClear = (*s)[:index], (*s)[index:] |
237 | -
for len(toClear) > 0 { |
238 | -
toClear = toClear[copy(toClear, nilChildren):] |
239 | -
} |
240 | -
} |
241 | -
|
242 |
// node is an internal node in a tree. |
243 |
// |
244 |
// It must at all times maintain the invariant that either |
245 |
// * len(children) == 0, len(items) unconstrained |
246 |
// * len(children) == len(items) + 1 |
247 | -
type node struct { |
248 | -
items items |
249 | -
children |
250 | -
cow *copyOnWriteContext |
251 |
} |
252 | |
253 | -
func (n *node) mutableFor(cow *copyOnWriteContext) *node { |
254 |
if n.cow == cow { |
255 |
return n |
256 |
} |
@@ -258,20 +240,20 @@ | |
258 |
if cap(out.items) >= len(n.items) { |
259 |
out.items = out.items[:len(n.items)] |
260 |
} else { |
261 | -
out.items = make(items, len(n.items), cap(n.items)) |
262 |
} |
263 |
copy(out.items, n.items) |
264 |
// Copy children |
265 |
if cap(out.children) >= len(n.children) { |
266 |
out.children = out.children[:len(n.children)] |
267 |
} else { |
268 | -
out.children = make( |
269 |
} |
270 |
copy(out.children, n.children) |
271 |
return out |
272 |
} |
273 | |
274 | -
func (n *node) mutableChild(i int) *node { |
275 |
c := n.children[i].mutableFor(n.cow) |
276 |
n.children[i] = c |
277 |
return c |
@@ -280,7 +262,7 @@ | |
280 |
// split splits the given node at the given index. The current node shrinks, |
281 |
// and this function returns the item that existed at that index and a new node |
282 |
// containing all items/children after it. |
283 | -
func (n *node) split(i int) ( |
284 |
item := n.items[i] |
285 |
next := n.cow.newNode() |
286 |
next.items = append(next.items, n.items[i+1:]...) |
@@ -294,7 +276,7 @@ | |
294 | |
295 |
// maybeSplitChild checks if a child should be split, and if so splits it. |
296 |
// Returns whether or not a split occurred. |
297 | -
func (n *node) maybeSplitChild(i, maxItems int) bool { |
298 |
if len(n.children[i].items) < maxItems { |
299 |
return false |
300 |
} |
@@ -308,70 +290,70 @@ | |
308 |
// insert inserts an item into the subtree rooted at this node, making sure |
309 |
// no nodes in the subtree exceed maxItems items. Should an equivalent item be |
310 |
// be found/replaced by insert, it will be returned. |
311 | -
func (n *node) insert(item |
312 | -
i, found := n.items.find(item) |
313 |
if found { |
314 |
out := n.items[i] |
315 |
n.items[i] = item |
316 | -
return out |
317 |
} |
318 |
if len(n.children) == 0 { |
319 |
n.items.insertAt(i, item) |
320 | -
return |
321 |
} |
322 |
if n.maybeSplitChild(i, maxItems) { |
323 |
inTree := n.items[i] |
324 |
switch { |
325 | -
case |
326 |
// no change, we want first split node |
327 | -
case |
328 |
i++ // we want second split node |
329 |
default: |
330 |
out := n.items[i] |
331 |
n.items[i] = item |
332 | -
return out |
333 |
} |
334 |
} |
335 |
return n.mutableChild(i).insert(item, maxItems) |
336 |
} |
337 | |
338 |
// get finds the given key in the subtree and returns it. |
339 | -
func (n *node) get(key |
340 | -
i, found := n.items.find(key) |
341 |
if found { |
342 | -
return n.items[i] |
343 |
} else if len(n.children) > 0 { |
344 |
return n.children[i].get(key) |
345 |
} |
346 | -
return |
347 |
} |
348 | |
349 |
// min returns the first item in the subtree. |
350 | -
func min(n *node) |
351 |
if n == nil { |
352 | -
return |
353 |
} |
354 |
for len(n.children) > 0 { |
355 |
n = n.children[0] |
356 |
} |
357 |
if len(n.items) == 0 { |
358 | -
return |
359 |
} |
360 | -
return n.items[0] |
361 |
} |
362 | |
363 |
// max returns the last item in the subtree. |
364 | -
func max(n *node) |
365 |
if n == nil { |
366 | -
return |
367 |
} |
368 |
for len(n.children) > 0 { |
369 |
n = n.children[len(n.children)-1] |
370 |
} |
371 |
if len(n.items) == 0 { |
372 | -
return |
373 |
} |
374 | -
return n.items[len(n.items)-1] |
375 |
} |
376 | |
377 |
// toRemove details what item to remove in a node.remove call. |
@@ -384,27 +366,27 @@ | |
384 |
) |
385 | |
386 |
// remove removes an item from the subtree rooted at this node. |
387 | -
func (n *node) remove(item |
388 |
var i int |
389 |
var found bool |
390 |
switch typ { |
391 |
case removeMax: |
392 |
if len(n.children) == 0 { |
393 | -
return n.items.pop() |
394 |
} |
395 |
i = len(n.items) |
396 |
case removeMin: |
397 |
if len(n.children) == 0 { |
398 | -
return n.items.removeAt(0) |
399 |
} |
400 |
i = 0 |
401 |
case removeItem: |
402 | -
i, found = n.items.find(item) |
403 |
if len(n.children) == 0 { |
404 |
if found { |
405 | -
return n.items.removeAt(i) |
406 |
} |
407 | -
return |
408 |
} |
409 |
default: |
410 |
panic("invalid type") |
@@ -424,8 +406,9 @@ | |
424 |
// We use our special-case 'remove' call with typ=maxItem to pull the |
425 |
// predecessor of item i (the rightmost leaf of our immediate left child) |
426 |
// and set it into where we pulled the item from. |
427 | -
|
428 | -
|
429 |
} |
430 |
// Final recursive call. Once we're here, we know that the item isn't in this |
431 |
// node and that the child is big enough to remove from. |
@@ -451,7 +434,7 @@ | |
451 |
// We then simply redo our remove call, and the second time (regardless of |
452 |
// whether we're in case 1 or 2), we'll have enough items and can guarantee |
453 |
// that we hit case A. |
454 | -
func (n *node) growChildAndRemove(i int, item |
455 |
if i > 0 && len(n.children[i-1].items) > minItems { |
456 |
// Steal from left child |
457 |
child := n.mutableChild(i) |
@@ -495,6 +478,18 @@ | |
495 |
ascend = direction(+1) |
496 |
) |
497 | |
498 |
// iterate provides a simple method for iterating over elements in the tree. |
499 |
// |
500 |
// When ascending, the 'start' should be less than 'stop' and when descending, |
@@ -502,13 +497,13 @@ | |
502 |
// will force the iterator to include the first item when it equals 'start', |
503 |
// thus creating a "greaterOrEqual" or "lessThanEqual" rather than just a |
504 |
// "greaterThan" or "lessThan" queries. |
505 | -
func (n *node) iterate(dir direction, start, stop |
506 |
var ok, found bool |
507 |
var index int |
508 |
switch dir { |
509 |
case ascend: |
510 | -
if start |
511 | -
index, _ = n.items.find(start) |
512 |
} |
513 |
for i := index; i < len(n.items); i++ { |
514 |
if len(n.children) > 0 { |
@@ -516,12 +511,12 @@ | |
516 |
return hit, false |
517 |
} |
518 |
} |
519 | -
if !includeStart && !hit && start |
520 |
hit = true |
521 |
continue |
522 |
} |
523 |
hit = true |
524 | -
if stop |
525 |
return hit, false |
526 |
} |
527 |
if !iter(n.items[i]) { |
@@ -534,8 +529,8 @@ | |
534 |
} |
535 |
} |
536 |
case descend: |
537 | -
if start |
538 | -
index, found = n.items.find(start) |
539 |
if !found { |
540 |
index = index - 1 |
541 |
} |
@@ -543,8 +538,8 @@ | |
543 |
index = len(n.items) - 1 |
544 |
} |
545 |
for i := index; i >= 0; i-- { |
546 | -
if start |
547 | -
if !includeStart || hit || start. |
548 |
continue |
549 |
} |
550 |
} |
@@ -553,7 +548,7 @@ | |
553 |
return hit, false |
554 |
} |
555 |
} |
556 | -
if stop |
557 |
return hit, false // continue |
558 |
} |
559 |
hit = true |
@@ -570,28 +565,32 @@ | |
570 |
return hit, true |
571 |
} |
572 | |
573 | -
// |
574 | -
func (n *node) print(w io.Writer, level int) { |
575 |
fmt.Fprintf(w, "%sNODE:%v\n", strings.Repeat(" ", level), n.items) |
576 |
for _, c := range n.children { |
577 |
c.print(w, level+1) |
578 |
} |
579 |
} |
580 | |
581 | -
// |
582 |
// |
583 | -
// |
584 |
// removal, and iteration. |
585 |
// |
586 |
// Write operations are not safe for concurrent mutation by multiple |
587 |
// goroutines, but Read operations are. |
588 | -
type |
589 |
degree int |
590 |
length int |
591 | -
root *node |
592 | -
cow *copyOnWriteContext |
593 |
} |
594 | |
595 |
// copyOnWriteContext pointers determine node ownership... a tree with a write |
596 |
// context equivalent to a node's write context is allowed to modify that node. |
597 |
// A tree whose write context does not match a node's is not allowed to modify |
@@ -606,8 +605,9 @@ | |
606 |
// tree's context, that node is modifiable in place. Children of that node may |
607 |
// not share context, but before we descend into them, we'll make a mutable |
608 |
// copy. |
609 | -
type copyOnWriteContext struct { |
610 | -
freelist * |
611 |
} |
612 | |
613 |
// Clone clones the btree, lazily. Clone should not be called concurrently, |
@@ -621,7 +621,7 @@ | |
621 |
// will initially experience minor slow-downs caused by additional allocs and |
622 |
// copies due to the aforementioned copy-on-write logic, but should converge to |
623 |
// the original performance characteristics of the original tree. |
624 | -
func (t * |
625 |
// Create two entirely new copy-on-write contexts. |
626 |
// This operation effectively creates three trees: |
627 |
// the original, shared nodes (old b.cow) |
@@ -635,17 +635,17 @@ | |
635 |
} |
636 | |
637 |
// maxItems returns the max number of items to allow per node. |
638 | -
func (t * |
639 |
return t.degree*2 - 1 |
640 |
} |
641 | |
642 |
// minItems returns the min number of items to allow per node (ignored for the |
643 |
// root node). |
644 | -
func (t * |
645 |
return t.degree - 1 |
646 |
} |
647 | |
648 | -
func (c *copyOnWriteContext) newNode() (n *node) { |
649 |
n = c.freelist.newNode() |
650 |
n.cow = c |
651 |
return |
@@ -662,7 +662,7 @@ | |
662 |
// freeNode frees a node within a given COW context, if it's owned by that |
663 |
// context. It returns what happened to the node (see freeType const |
664 |
// documentation). |
665 | -
func (c *copyOnWriteContext) freeNode(n *node) freeType { |
666 |
if n.cow == c { |
667 |
// clear to allow GC |
668 |
n.items.truncate(0) |
@@ -679,19 +679,16 @@ | |
679 |
} |
680 | |
681 |
// ReplaceOrInsert adds the given item to the tree. If an item in the tree |
682 | -
// already equals the given one, it is removed from the tree and returned |
683 | -
// |
684 |
// |
685 |
// nil cannot be added to the tree (will panic). |
686 | -
func (t * |
687 | -
if item == nil { |
688 | -
panic("nil item being added to BTree") |
689 | -
} |
690 |
if t.root == nil { |
691 |
t.root = t.cow.newNode() |
692 |
t.root.items = append(t.root.items, item) |
693 |
t.length++ |
694 | -
return |
695 |
} else { |
696 |
t.root = t.root.mutableFor(t.cow) |
697 |
if len(t.root.items) >= t.maxItems() { |
@@ -702,146 +699,149 @@ | |
702 |
t.root.children = append(t.root.children, oldroot, second) |
703 |
} |
704 |
} |
705 | -
out := t.root.insert(item, t.maxItems()) |
706 | -
if |
707 |
t.length++ |
708 |
} |
709 | -
return out |
710 |
} |
711 | |
712 |
// Delete removes an item equal to the passed in item from the tree, returning |
713 | -
// it. If no such item exists, returns |
714 | -
func (t * |
715 |
return t.deleteItem(item, removeItem) |
716 |
} |
717 | |
718 |
// DeleteMin removes the smallest item in the tree and returns it. |
719 | -
// If no such item exists, returns |
720 | -
func (t * |
721 | -
|
722 |
} |
723 | |
724 |
// DeleteMax removes the largest item in the tree and returns it. |
725 | -
// If no such item exists, returns |
726 | -
func (t * |
727 | -
|
728 |
} |
729 | |
730 | -
func (t * |
731 |
if t.root == nil || len(t.root.items) == 0 { |
732 | -
return |
733 |
} |
734 |
t.root = t.root.mutableFor(t.cow) |
735 | -
out := t.root.remove(item, t.minItems(), typ) |
736 |
if len(t.root.items) == 0 && len(t.root.children) > 0 { |
737 |
oldroot := t.root |
738 |
t.root = t.root.children[0] |
739 |
t.cow.freeNode(oldroot) |
740 |
} |
741 | -
if |
742 |
t.length-- |
743 |
} |
744 | -
return out |
745 |
} |
746 | |
747 |
// AscendRange calls the iterator for every value in the tree within the range |
748 |
// [greaterOrEqual, lessThan), until iterator returns false. |
749 | -
func (t * |
750 |
if t.root == nil { |
751 |
return |
752 |
} |
753 | -
t.root.iterate(ascend, greaterOrEqual, lessThan, true, false, iterator) |
754 |
} |
755 | |
756 |
// AscendLessThan calls the iterator for every value in the tree within the range |
757 |
// [first, pivot), until iterator returns false. |
758 | -
func (t * |
759 |
if t.root == nil { |
760 |
return |
761 |
} |
762 | -
t.root.iterate(ascend, |
763 |
} |
764 | |
765 |
// AscendGreaterOrEqual calls the iterator for every value in the tree within |
766 |
// the range [pivot, last], until iterator returns false. |
767 | -
func (t * |
768 |
if t.root == nil { |
769 |
return |
770 |
} |
771 | -
t.root.iterate(ascend, pivot, |
772 |
} |
773 | |
774 |
// Ascend calls the iterator for every value in the tree within the range |
775 |
// [first, last], until iterator returns false. |
776 | -
func (t * |
777 |
if t.root == nil { |
778 |
return |
779 |
} |
780 | -
t.root.iterate(ascend, |
781 |
} |
782 | |
783 |
// DescendRange calls the iterator for every value in the tree within the range |
784 |
// [lessOrEqual, greaterThan), until iterator returns false. |
785 | -
func (t * |
786 |
if t.root == nil { |
787 |
return |
788 |
} |
789 | -
t.root.iterate(descend, lessOrEqual, greaterThan, true, false, iterator) |
790 |
} |
791 | |
792 |
// DescendLessOrEqual calls the iterator for every value in the tree within the range |
793 |
// [pivot, first], until iterator returns false. |
794 | -
func (t * |
795 |
if t.root == nil { |
796 |
return |
797 |
} |
798 | -
t.root.iterate(descend, pivot, |
799 |
} |
800 | |
801 |
// DescendGreaterThan calls the iterator for every value in the tree within |
802 |
// the range [last, pivot), until iterator returns false. |
803 | -
func (t * |
804 |
if t.root == nil { |
805 |
return |
806 |
} |
807 | -
t.root.iterate(descend, |
808 |
} |
809 | |
810 |
// Descend calls the iterator for every value in the tree within the range |
811 |
// [last, first], until iterator returns false. |
812 | -
func (t * |
813 |
if t.root == nil { |
814 |
return |
815 |
} |
816 | -
t.root.iterate(descend, |
817 |
} |
818 | |
819 | -
// Get looks for the key item in the tree, returning it. It returns |
820 | -
// unable to find that item. |
821 | -
func (t * |
822 |
if t.root == nil { |
823 | -
return |
824 |
} |
825 |
return t.root.get(key) |
826 |
} |
827 | |
828 | -
// Min returns the smallest item in the tree, or |
829 | -
func (t * |
830 |
return min(t.root) |
831 |
} |
832 | |
833 | -
// Max returns the largest item in the tree, or |
834 | -
func (t * |
835 |
return max(t.root) |
836 |
} |
837 | |
838 |
// Has returns true if the given key is in the tree. |
839 | -
func (t * |
840 | -
|
841 |
} |
842 | |
843 |
// Len returns the number of items currently in the tree. |
844 | -
func (t * |
845 |
return t.length |
846 |
} |
847 | |
@@ -865,7 +865,7 @@ | |
865 |
// O(tree size): when all nodes are owned by another tree, all nodes are |
866 |
// iterated over looking for nodes to add to the freelist, and due to |
867 |
// ownership, none are. |
868 | -
func (t * |
869 |
if t.root != nil && addNodesToFreelist { |
870 |
t.root.reset(t.cow) |
871 |
} |
@@ -875,7 +875,7 @@ | |
875 |
// reset returns a subtree to the freelist. It breaks out immediately if the |
876 |
// freelist is full, since the only benefit of iterating is to fill that |
877 |
// freelist up. Returns true if parent reset call should continue. |
878 | -
func (n *node) reset(c *copyOnWriteContext) bool { |
879 |
for _, child := range n.children { |
880 |
if !child.reset(c) { |
881 |
return false |
@@ -891,3 +891,193 @@ | |
891 |
func (a Int) Less(b Item) bool { |
892 |
return a < b.(Int) |
893 |
} |
1 | +
// Copyright 2014-2022 Google Inc. |
2 |
// |
3 |
// Licensed under the Apache License, Version 2.0 (the "License"); |
4 |
// you may not use this file except in compliance with the License. |
12 |
// See the License for the specific language governing permissions and |
13 |
// limitations under the License. |
14 | |
15 | +
//go:build go1.18 |
16 | +
// +build go1.18 |
17 | +
|
18 | +
// In Go 1.18 and beyond, a BTreeG generic is created, and BTree is a specific |
19 | +
// instantiation of that generic for the Item interface, with a backwards- |
20 | +
// compatible API. Before go1.18, generics are not supported, |
21 | +
// and BTree is just an implementation based around the Item interface. |
22 | |
23 |
// Package btree implements in-memory B-Trees of arbitrary degree. |
24 |
// |
53 |
// Its functions, therefore, exactly mirror those of |
54 |
// llrb.LLRB where possible. Unlike gollrb, though, we currently don't |
55 |
// support storing multiple equivalent values. |
56 | +
// |
57 | +
// There are two implementations; those suffixed with 'G' are generics, usable |
58 | +
// for any type, and require a passed-in "less" function to define their ordering. |
59 | +
// Those without this prefix are specific to the 'Item' interface, and use |
60 | +
// its 'Less' function for ordering. |
61 |
package btree |
62 | |
63 |
import ( |
82 |
DefaultFreeListSize = 32 |
83 |
) |
84 | |
85 | +
// FreeListG represents a free list of btree nodes. By default each |
86 |
// BTree has its own FreeList, but multiple BTrees can share the same |
87 | +
// FreeList, in particular when they're created with Clone. |
88 |
// Two Btrees using the same freelist are safe for concurrent write access. |
89 | +
type FreeListG[T any] struct { |
90 |
mu sync.Mutex |
91 | +
freelist []*node[T] |
92 |
} |
93 | |
94 | +
// NewFreeListG creates a new free list. |
95 |
// size is the maximum size of the returned free list. |
96 | +
func NewFreeListG[T any](size int) *FreeListG[T] { |
97 | +
return &FreeListG[T]{freelist: make([]*node[T], 0, size)} |
98 |
} |
99 | |
100 | +
func (f *FreeListG[T]) newNode() (n *node[T]) { |
101 |
f.mu.Lock() |
102 |
index := len(f.freelist) - 1 |
103 |
if index < 0 { |
104 |
f.mu.Unlock() |
105 | +
return new(node[T]) |
106 |
} |
107 |
n = f.freelist[index] |
108 |
f.freelist[index] = nil |
111 |
return |
112 |
} |
113 | |
114 | +
func (f *FreeListG[T]) freeNode(n *node[T]) (out bool) { |
115 |
f.mu.Lock() |
116 |
if len(f.freelist) < cap(f.freelist) { |
117 |
f.freelist = append(f.freelist, n) |
121 |
return |
122 |
} |
123 | |
124 | +
// ItemIteratorG allows callers of {A/De}scend* to iterate in-order over portions of |
125 |
// the tree. When this function returns false, iteration will stop and the |
126 |
// associated Ascend* function will immediately return. |
127 | +
type ItemIteratorG[T any] func(item T) bool |
128 | |
129 | +
// Ordered represents the set of types for which the '<' operator work. |
130 | +
type Ordered interface { |
131 | +
~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~float32 | ~float64 | ~string |
132 | +
} |
133 | +
|
134 | +
// Less[T] returns a default LessFunc that uses the '<' operator for types that support it. |
135 | +
func Less[T Ordered]() LessFunc[T] { |
136 | +
return func(a, b T) bool { return a < b } |
137 | +
} |
138 | +
|
139 | +
// NewOrderedG creates a new B-Tree for ordered types. |
140 | +
func NewOrderedG[T Ordered](degree int) *BTreeG[T] { |
141 | +
return NewG[T](degree, Less[T]()) |
142 | +
} |
143 | +
|
144 | +
// NewG creates a new B-Tree with the given degree. |
145 |
// |
146 | +
// NewG(2), for example, will create a 2-3-4 tree (each node contains 1-3 items |
147 |
// and 2-4 children). |
148 | +
// |
149 | +
// The passed-in LessFunc determines how objects of type T are ordered. |
150 | +
func NewG[T any](degree int, less LessFunc[T]) *BTreeG[T] { |
151 | +
return NewWithFreeListG(degree, less, NewFreeListG[T](DefaultFreeListSize)) |
152 |
} |
153 | |
154 | +
// NewWithFreeListG creates a new B-Tree that uses the given node free list. |
155 | +
func NewWithFreeListG[T any](degree int, less LessFunc[T], f *FreeListG[T]) *BTreeG[T] { |
156 |
if degree <= 1 { |
157 |
panic("bad degree") |
158 |
} |
159 | +
return &BTreeG[T]{ |
160 |
degree: degree, |
161 | +
cow: ©OnWriteContext[T]{freelist: f, less: less}, |
162 |
} |
163 |
} |
164 | |
165 |
// items stores items in a node. |
166 | +
type items[T any] []T |
167 | |
168 |
// insertAt inserts a value into the given index, pushing all subsequent values |
169 |
// forward. |
170 | +
func (s *items[T]) insertAt(index int, item T) { |
171 | +
var zero T |
172 | +
*s = append(*s, zero) |
173 |
if index < len(*s) { |
174 |
copy((*s)[index+1:], (*s)[index:]) |
175 |
} |
178 | |
179 |
// removeAt removes a value at a given index, pulling all subsequent values |
180 |
// back. |
181 | +
func (s *items[T]) removeAt(index int) T { |
182 |
item := (*s)[index] |
183 |
copy((*s)[index:], (*s)[index+1:]) |
184 | +
var zero T |
185 | +
(*s)[len(*s)-1] = zero |
186 |
*s = (*s)[:len(*s)-1] |
187 |
return item |
188 |
} |
189 | |
190 |
// pop removes and returns the last element in the list. |
191 | +
func (s *items[T]) pop() (out T) { |
192 |
index := len(*s) - 1 |
193 |
out = (*s)[index] |
194 | +
var zero T |
195 | +
(*s)[index] = zero |
196 |
*s = (*s)[:index] |
197 |
return |
198 |
} |
199 | |
200 |
// truncate truncates this instance at index so that it contains only the |
201 |
// first index items. index must be less than or equal to length. |
202 | +
func (s *items[T]) truncate(index int) { |
203 | +
var toClear items[T] |
204 |
*s, toClear = (*s)[:index], (*s)[index:] |
205 | +
var zero T |
206 | +
for i := 0; i < len(toClear); i++ { |
207 | +
toClear[i] = zero |
208 |
} |
209 |
} |
210 | |
211 |
// find returns the index where the given item should be inserted into this |
212 |
// list. 'found' is true if the item already exists in the list at the given |
213 |
// index. |
214 | +
func (s items[T]) find(item T, less func(T, T) bool) (index int, found bool) { |
215 |
i := sort.Search(len(s), func(i int) bool { |
216 | +
return less(item, s[i]) |
217 |
}) |
218 | +
if i > 0 && !less(s[i-1], item) { |
219 |
return i - 1, true |
220 |
} |
221 |
return i, false |
222 |
} |
223 | |
224 |
// node is an internal node in a tree. |
225 |
// |
226 |
// It must at all times maintain the invariant that either |
227 |
// * len(children) == 0, len(items) unconstrained |
228 |
// * len(children) == len(items) + 1 |
229 | +
type node[T any] struct { |
230 | +
items items[T] |
231 | +
children items[*node[T]] |
232 | +
cow *copyOnWriteContext[T] |
233 |
} |
234 | |
235 | +
func (n *node[T]) mutableFor(cow *copyOnWriteContext[T]) *node[T] { |
236 |
if n.cow == cow { |
237 |
return n |
238 |
} |
240 |
if cap(out.items) >= len(n.items) { |
241 |
out.items = out.items[:len(n.items)] |
242 |
} else { |
243 | +
out.items = make(items[T], len(n.items), cap(n.items)) |
244 |
} |
245 |
copy(out.items, n.items) |
246 |
// Copy children |
247 |
if cap(out.children) >= len(n.children) { |
248 |
out.children = out.children[:len(n.children)] |
249 |
} else { |
250 | +
out.children = make(items[*node[T]], len(n.children), cap(n.children)) |
251 |
} |
252 |
copy(out.children, n.children) |
253 |
return out |
254 |
} |
255 | |
256 | +
func (n *node[T]) mutableChild(i int) *node[T] { |
257 |
c := n.children[i].mutableFor(n.cow) |
258 |
n.children[i] = c |
259 |
return c |
262 |
// split splits the given node at the given index. The current node shrinks, |
263 |
// and this function returns the item that existed at that index and a new node |
264 |
// containing all items/children after it. |
265 | +
func (n *node[T]) split(i int) (T, *node[T]) { |
266 |
item := n.items[i] |
267 |
next := n.cow.newNode() |
268 |
next.items = append(next.items, n.items[i+1:]...) |
276 | |
277 |
// maybeSplitChild checks if a child should be split, and if so splits it. |
278 |
// Returns whether or not a split occurred. |
279 | +
func (n *node[T]) maybeSplitChild(i, maxItems int) bool { |
280 |
if len(n.children[i].items) < maxItems { |
281 |
return false |
282 |
} |
290 |
// insert inserts an item into the subtree rooted at this node, making sure |
291 |
// no nodes in the subtree exceed maxItems items. Should an equivalent item be |
292 |
// be found/replaced by insert, it will be returned. |
293 | +
func (n *node[T]) insert(item T, maxItems int) (_ T, _ bool) { |
294 | +
i, found := n.items.find(item, n.cow.less) |
295 |
if found { |
296 |
out := n.items[i] |
297 |
n.items[i] = item |
298 | +
return out, true |
299 |
} |
300 |
if len(n.children) == 0 { |
301 |
n.items.insertAt(i, item) |
302 | +
return |
303 |
} |
304 |
if n.maybeSplitChild(i, maxItems) { |
305 |
inTree := n.items[i] |
306 |
switch { |
307 | +
case n.cow.less(item, inTree): |
308 |
// no change, we want first split node |
309 | +
case n.cow.less(inTree, item): |
310 |
i++ // we want second split node |
311 |
default: |
312 |
out := n.items[i] |
313 |
n.items[i] = item |
314 | +
return out, true |
315 |
} |
316 |
} |
317 |
return n.mutableChild(i).insert(item, maxItems) |
318 |
} |
319 | |
320 |
// get finds the given key in the subtree and returns it. |
321 | +
func (n *node[T]) get(key T) (_ T, _ bool) { |
322 | +
i, found := n.items.find(key, n.cow.less) |
323 |
if found { |
324 | +
return n.items[i], true |
325 |
} else if len(n.children) > 0 { |
326 |
return n.children[i].get(key) |
327 |
} |
328 | +
return |
329 |
} |
330 | |
331 |
// min returns the first item in the subtree. |
332 | +
func min[T any](n *node[T]) (_ T, found bool) { |
333 |
if n == nil { |
334 | +
return |
335 |
} |
336 |
for len(n.children) > 0 { |
337 |
n = n.children[0] |
338 |
} |
339 |
if len(n.items) == 0 { |
340 | +
return |
341 |
} |
342 | +
return n.items[0], true |
343 |
} |
344 | |
345 |
// max returns the last item in the subtree. |
346 | +
func max[T any](n *node[T]) (_ T, found bool) { |
347 |
if n == nil { |
348 | +
return |
349 |
} |
350 |
for len(n.children) > 0 { |
351 |
n = n.children[len(n.children)-1] |
352 |
} |
353 |
if len(n.items) == 0 { |
354 | +
return |
355 |
} |
356 | +
return n.items[len(n.items)-1], true |
357 |
} |
358 | |
359 |
// toRemove details what item to remove in a node.remove call. |
366 |
) |
367 | |
368 |
// remove removes an item from the subtree rooted at this node. |
369 | +
func (n *node[T]) remove(item T, minItems int, typ toRemove) (_ T, _ bool) { |
370 |
var i int |
371 |
var found bool |
372 |
switch typ { |
373 |
case removeMax: |
374 |
if len(n.children) == 0 { |
375 | +
return n.items.pop(), true |
376 |
} |
377 |
i = len(n.items) |
378 |
case removeMin: |
379 |
if len(n.children) == 0 { |
380 | +
return n.items.removeAt(0), true |
381 |
} |
382 |
i = 0 |
383 |
case removeItem: |
384 | +
i, found = n.items.find(item, n.cow.less) |
385 |
if len(n.children) == 0 { |
386 |
if found { |
387 | +
return n.items.removeAt(i), true |
388 |
} |
389 | +
return |
390 |
} |
391 |
default: |
392 |
panic("invalid type") |
406 |
// We use our special-case 'remove' call with typ=maxItem to pull the |
407 |
// predecessor of item i (the rightmost leaf of our immediate left child) |
408 |
// and set it into where we pulled the item from. |
409 | +
var zero T |
410 | +
n.items[i], _ = child.remove(zero, minItems, removeMax) |
411 | +
return out, true |
412 |
} |
413 |
// Final recursive call. Once we're here, we know that the item isn't in this |
414 |
// node and that the child is big enough to remove from. |
434 |
// We then simply redo our remove call, and the second time (regardless of |
435 |
// whether we're in case 1 or 2), we'll have enough items and can guarantee |
436 |
// that we hit case A. |
437 | +
func (n *node[T]) growChildAndRemove(i int, item T, minItems int, typ toRemove) (T, bool) { |
438 |
if i > 0 && len(n.children[i-1].items) > minItems { |
439 |
// Steal from left child |
440 |
child := n.mutableChild(i) |
478 |
ascend = direction(+1) |
479 |
) |
480 | |
481 | +
type optionalItem[T any] struct { |
482 | +
item T |
483 | +
valid bool |
484 | +
} |
485 | +
|
486 | +
func optional[T any](item T) optionalItem[T] { |
487 | +
return optionalItem[T]{item: item, valid: true} |
488 | +
} |
489 | +
func empty[T any]() optionalItem[T] { |
490 | +
return optionalItem[T]{} |
491 | +
} |
492 | +
|
493 |
// iterate provides a simple method for iterating over elements in the tree. |
494 |
// |
495 |
// When ascending, the 'start' should be less than 'stop' and when descending, |
497 |
// will force the iterator to include the first item when it equals 'start', |
498 |
// thus creating a "greaterOrEqual" or "lessThanEqual" rather than just a |
499 |
// "greaterThan" or "lessThan" queries. |
500 | +
func (n *node[T]) iterate(dir direction, start, stop optionalItem[T], includeStart bool, hit bool, iter ItemIteratorG[T]) (bool, bool) { |
501 |
var ok, found bool |
502 |
var index int |
503 |
switch dir { |
504 |
case ascend: |
505 | +
if start.valid { |
506 | +
index, _ = n.items.find(start.item, n.cow.less) |
507 |
} |
508 |
for i := index; i < len(n.items); i++ { |
509 |
if len(n.children) > 0 { |
511 |
return hit, false |
512 |
} |
513 |
} |
514 | +
if !includeStart && !hit && start.valid && !n.cow.less(start.item, n.items[i]) { |
515 |
hit = true |
516 |
continue |
517 |
} |
518 |
hit = true |
519 | +
if stop.valid && !n.cow.less(n.items[i], stop.item) { |
520 |
return hit, false |
521 |
} |
522 |
if !iter(n.items[i]) { |
529 |
} |
530 |
} |
531 |
case descend: |
532 | +
if start.valid { |
533 | +
index, found = n.items.find(start.item, n.cow.less) |
534 |
if !found { |
535 |
index = index - 1 |
536 |
} |
538 |
index = len(n.items) - 1 |
539 |
} |
540 |
for i := index; i >= 0; i-- { |
541 | +
if start.valid && !n.cow.less(n.items[i], start.item) { |
542 | +
if !includeStart || hit || n.cow.less(start.item, n.items[i]) { |
543 |
continue |
544 |
} |
545 |
} |
548 |
return hit, false |
549 |
} |
550 |
} |
551 | +
if stop.valid && !n.cow.less(stop.item, n.items[i]) { |
552 |
return hit, false // continue |
553 |
} |
554 |
hit = true |
565 |
return hit, true |
566 |
} |
567 | |
568 | +
// print is used for testing/debugging purposes. |
569 | +
func (n *node[T]) print(w io.Writer, level int) { |
570 |
fmt.Fprintf(w, "%sNODE:%v\n", strings.Repeat(" ", level), n.items) |
571 |
for _, c := range n.children { |
572 |
c.print(w, level+1) |
573 |
} |
574 |
} |
575 | |
576 | +
// BTreeG is a generic implementation of a B-Tree. |
577 |
// |
578 | +
// BTreeG stores items of type T in an ordered structure, allowing easy insertion, |
579 |
// removal, and iteration. |
580 |
// |
581 |
// Write operations are not safe for concurrent mutation by multiple |
582 |
// goroutines, but Read operations are. |
583 | +
type BTreeG[T any] struct { |
584 |
degree int |
585 |
length int |
586 | +
root *node[T] |
587 | +
cow *copyOnWriteContext[T] |
588 |
} |
589 | |
590 | +
// LessFunc[T] determines how to order a type 'T'. It should implement a strict |
591 | +
// ordering, and should return true if within that ordering, 'a' < 'b'. |
592 | +
type LessFunc[T any] func(a, b T) bool |
593 | +
|
594 |
// copyOnWriteContext pointers determine node ownership... a tree with a write |
595 |
// context equivalent to a node's write context is allowed to modify that node. |
596 |
// A tree whose write context does not match a node's is not allowed to modify |
605 |
// tree's context, that node is modifiable in place. Children of that node may |
606 |
// not share context, but before we descend into them, we'll make a mutable |
607 |
// copy. |
608 | +
type copyOnWriteContext[T any] struct { |
609 | +
freelist *FreeListG[T] |
610 | +
less LessFunc[T] |
611 |
} |
612 | |
613 |
// Clone clones the btree, lazily. Clone should not be called concurrently, |
621 |
// will initially experience minor slow-downs caused by additional allocs and |
622 |
// copies due to the aforementioned copy-on-write logic, but should converge to |
623 |
// the original performance characteristics of the original tree. |
624 | +
func (t *BTreeG[T]) Clone() (t2 *BTreeG[T]) { |
625 |
// Create two entirely new copy-on-write contexts. |
626 |
// This operation effectively creates three trees: |
627 |
// the original, shared nodes (old b.cow) |
635 |
} |
636 | |
637 |
// maxItems returns the max number of items to allow per node. |
638 | +
func (t *BTreeG[T]) maxItems() int { |
639 |
return t.degree*2 - 1 |
640 |
} |
641 | |
642 |
// minItems returns the min number of items to allow per node (ignored for the |
643 |
// root node). |
644 | +
func (t *BTreeG[T]) minItems() int { |
645 |
return t.degree - 1 |
646 |
} |
647 | |
648 | +
func (c *copyOnWriteContext[T]) newNode() (n *node[T]) { |
649 |
n = c.freelist.newNode() |
650 |
n.cow = c |
651 |
return |
662 |
// freeNode frees a node within a given COW context, if it's owned by that |
663 |
// context. It returns what happened to the node (see freeType const |
664 |
// documentation). |
665 | +
func (c *copyOnWriteContext[T]) freeNode(n *node[T]) freeType { |
666 |
if n.cow == c { |
667 |
// clear to allow GC |
668 |
n.items.truncate(0) |
679 |
} |
680 | |
681 |
// ReplaceOrInsert adds the given item to the tree. If an item in the tree |
682 | +
// already equals the given one, it is removed from the tree and returned, |
683 | +
// and the second return value is true. Otherwise, (zeroValue, false) |
684 |
// |
685 |
// nil cannot be added to the tree (will panic). |
686 | +
func (t *BTreeG[T]) ReplaceOrInsert(item T) (_ T, _ bool) { |
687 |
if t.root == nil { |
688 |
t.root = t.cow.newNode() |
689 |
t.root.items = append(t.root.items, item) |
690 |
t.length++ |
691 | +
return |
692 |
} else { |
693 |
t.root = t.root.mutableFor(t.cow) |
694 |
if len(t.root.items) >= t.maxItems() { |
699 |
t.root.children = append(t.root.children, oldroot, second) |
700 |
} |
701 |
} |
702 | +
out, outb := t.root.insert(item, t.maxItems()) |
703 | +
if !outb { |
704 |
t.length++ |
705 |
} |
706 | +
return out, outb |
707 |
} |
708 | |
709 |
// Delete removes an item equal to the passed in item from the tree, returning |
710 | +
// it. If no such item exists, returns (zeroValue, false). |
711 | +
func (t *BTreeG[T]) Delete(item T) (T, bool) { |
712 |
return t.deleteItem(item, removeItem) |
713 |
} |
714 | |
715 |
// DeleteMin removes the smallest item in the tree and returns it. |
716 | +
// If no such item exists, returns (zeroValue, false). |
717 | +
func (t *BTreeG[T]) DeleteMin() (T, bool) { |
718 | +
var zero T |
719 | +
return t.deleteItem(zero, removeMin) |
720 |
} |
721 | |
722 |
// DeleteMax removes the largest item in the tree and returns it. |
723 | +
// If no such item exists, returns (zeroValue, false). |
724 | +
func (t *BTreeG[T]) DeleteMax() (T, bool) { |
725 | +
var zero T |
726 | +
return t.deleteItem(zero, removeMax) |
727 |
} |
728 | |
729 | +
func (t *BTreeG[T]) deleteItem(item T, typ toRemove) (_ T, _ bool) { |
730 |
if t.root == nil || len(t.root.items) == 0 { |
731 | +
return |
732 |
} |
733 |
t.root = t.root.mutableFor(t.cow) |
734 | +
out, outb := t.root.remove(item, t.minItems(), typ) |
735 |
if len(t.root.items) == 0 && len(t.root.children) > 0 { |
736 |
oldroot := t.root |
737 |
t.root = t.root.children[0] |
738 |
t.cow.freeNode(oldroot) |
739 |
} |
740 | +
if outb { |
741 |
t.length-- |
742 |
} |
743 | +
return out, outb |
744 |
} |
745 | |
746 |
// AscendRange calls the iterator for every value in the tree within the range |
747 |
// [greaterOrEqual, lessThan), until iterator returns false. |
748 | +
func (t *BTreeG[T]) AscendRange(greaterOrEqual, lessThan T, iterator ItemIteratorG[T]) { |
749 |
if t.root == nil { |
750 |
return |
751 |
} |
752 | +
t.root.iterate(ascend, optional[T](greaterOrEqual), optional[T](lessThan), true, false, iterator) |
753 |
} |
754 | |
755 |
// AscendLessThan calls the iterator for every value in the tree within the range |
756 |
// [first, pivot), until iterator returns false. |
757 | +
func (t *BTreeG[T]) AscendLessThan(pivot T, iterator ItemIteratorG[T]) { |
758 |
if t.root == nil { |
759 |
return |
760 |
} |
761 | +
t.root.iterate(ascend, empty[T](), optional(pivot), false, false, iterator) |
762 |
} |
763 | |
764 |
// AscendGreaterOrEqual calls the iterator for every value in the tree within |
765 |
// the range [pivot, last], until iterator returns false. |
766 | +
func (t *BTreeG[T]) AscendGreaterOrEqual(pivot T, iterator ItemIteratorG[T]) { |
767 |
if t.root == nil { |
768 |
return |
769 |
} |
770 | +
t.root.iterate(ascend, optional[T](pivot), empty[T](), true, false, iterator) |
771 |
} |
772 | |
773 |
// Ascend calls the iterator for every value in the tree within the range |
774 |
// [first, last], until iterator returns false. |
775 | +
func (t *BTreeG[T]) Ascend(iterator ItemIteratorG[T]) { |
776 |
if t.root == nil { |
777 |
return |
778 |
} |
779 | +
t.root.iterate(ascend, empty[T](), empty[T](), false, false, iterator) |
780 |
} |
781 | |
782 |
// DescendRange calls the iterator for every value in the tree within the range |
783 |
// [lessOrEqual, greaterThan), until iterator returns false. |
784 | +
func (t *BTreeG[T]) DescendRange(lessOrEqual, greaterThan T, iterator ItemIteratorG[T]) { |
785 |
if t.root == nil { |
786 |
return |
787 |
} |
788 | +
t.root.iterate(descend, optional[T](lessOrEqual), optional[T](greaterThan), true, false, iterator) |
789 |
} |
790 | |
791 |
// DescendLessOrEqual calls the iterator for every value in the tree within the range |
792 |
// [pivot, first], until iterator returns false. |
793 | +
func (t *BTreeG[T]) DescendLessOrEqual(pivot T, iterator ItemIteratorG[T]) { |
794 |
if t.root == nil { |
795 |
return |
796 |
} |
797 | +
t.root.iterate(descend, optional[T](pivot), empty[T](), true, false, iterator) |
798 |
} |
799 | |
800 |
// DescendGreaterThan calls the iterator for every value in the tree within |
801 |
// the range [last, pivot), until iterator returns false. |
802 | +
func (t *BTreeG[T]) DescendGreaterThan(pivot T, iterator ItemIteratorG[T]) { |
803 |
if t.root == nil { |
804 |
return |
805 |
} |
806 | +
t.root.iterate(descend, empty[T](), optional[T](pivot), false, false, iterator) |
807 |
} |
808 | |
809 |
// Descend calls the iterator for every value in the tree within the range |
810 |
// [last, first], until iterator returns false. |
811 | +
func (t *BTreeG[T]) Descend(iterator ItemIteratorG[T]) { |
812 |
if t.root == nil { |
813 |
return |
814 |
} |
815 | +
t.root.iterate(descend, empty[T](), empty[T](), false, false, iterator) |
816 |
} |
817 | |
818 | +
// Get looks for the key item in the tree, returning it. It returns |
819 | +
// (zeroValue, false) if unable to find that item. |
820 | +
func (t *BTreeG[T]) Get(key T) (_ T, _ bool) { |
821 |
if t.root == nil { |
822 | +
return |
823 |
} |
824 |
return t.root.get(key) |
825 |
} |
826 | |
827 | +
// Min returns the smallest item in the tree, or (zeroValue, false) if the tree is empty. |
828 | +
func (t *BTreeG[T]) Min() (_ T, _ bool) { |
829 |
return min(t.root) |
830 |
} |
831 | |
832 | +
// Max returns the largest item in the tree, or (zeroValue, false) if the tree is empty. |
833 | +
func (t *BTreeG[T]) Max() (_ T, _ bool) { |
834 |
return max(t.root) |
835 |
} |
836 | |
837 |
// Has returns true if the given key is in the tree. |
838 | +
func (t *BTreeG[T]) Has(key T) bool { |
839 | +
_, ok := t.Get(key) |
840 | +
return ok |
841 |
} |
842 | |
843 |
// Len returns the number of items currently in the tree. |
844 | +
func (t *BTreeG[T]) Len() int { |
845 |
return t.length |
846 |
} |
847 | |
865 |
// O(tree size): when all nodes are owned by another tree, all nodes are |
866 |
// iterated over looking for nodes to add to the freelist, and due to |
867 |
// ownership, none are. |
868 | +
func (t *BTreeG[T]) Clear(addNodesToFreelist bool) { |
869 |
if t.root != nil && addNodesToFreelist { |
870 |
t.root.reset(t.cow) |
871 |
} |
875 |
// reset returns a subtree to the freelist. It breaks out immediately if the |
876 |
// freelist is full, since the only benefit of iterating is to fill that |
877 |
// freelist up. Returns true if parent reset call should continue. |
878 | +
func (n *node[T]) reset(c *copyOnWriteContext[T]) bool { |
879 |
for _, child := range n.children { |
880 |
if !child.reset(c) { |
881 |
return false |
891 |
func (a Int) Less(b Item) bool { |
892 |
return a < b.(Int) |
893 |
} |
894 | +
|
895 | +
// BTree is an implementation of a B-Tree. |
896 | +
// |
897 | +
// BTree stores Item instances in an ordered structure, allowing easy insertion, |
898 | +
// removal, and iteration. |
899 | +
// |
900 | +
// Write operations are not safe for concurrent mutation by multiple |
901 | +
// goroutines, but Read operations are. |
902 | +
type BTree BTreeG[Item] |
903 | +
|
904 | +
var itemLess LessFunc[Item] = func(a, b Item) bool { |
905 | +
return a.Less(b) |
906 | +
} |
907 | +
|
908 | +
// New creates a new B-Tree with the given degree. |
909 | +
// |
910 | +
// New(2), for example, will create a 2-3-4 tree (each node contains 1-3 items |
911 | +
// and 2-4 children). |
912 | +
func New(degree int) *BTree { |
913 | +
return (*BTree)(NewG[Item](degree, itemLess)) |
914 | +
} |
915 | +
|
916 | +
// FreeList represents a free list of btree nodes. By default each |
917 | +
// BTree has its own FreeList, but multiple BTrees can share the same |
918 | +
// FreeList. |
919 | +
// Two Btrees using the same freelist are safe for concurrent write access. |
920 | +
type FreeList FreeListG[Item] |
921 | +
|
922 | +
// NewFreeList creates a new free list. |
923 | +
// size is the maximum size of the returned free list. |
924 | +
func NewFreeList(size int) *FreeList { |
925 | +
return (*FreeList)(NewFreeListG[Item](size)) |
926 | +
} |
927 | +
|
928 | +
// NewWithFreeList creates a new B-Tree that uses the given node free list. |
929 | +
func NewWithFreeList(degree int, f *FreeList) *BTree { |
930 | +
return (*BTree)(NewWithFreeListG[Item](degree, itemLess, (*FreeListG[Item])(f))) |
931 | +
} |
932 | +
|
933 | +
// ItemIterator allows callers of Ascend* to iterate in-order over portions of |
934 | +
// the tree. When this function returns false, iteration will stop and the |
935 | +
// associated Ascend* function will immediately return. |
936 | +
type ItemIterator ItemIteratorG[Item] |
937 | +
|
938 | +
// Clone clones the btree, lazily. Clone should not be called concurrently, |
939 | +
// but the original tree (t) and the new tree (t2) can be used concurrently |
940 | +
// once the Clone call completes. |
941 | +
// |
942 | +
// The internal tree structure of b is marked read-only and shared between t and |
943 | +
// t2. Writes to both t and t2 use copy-on-write logic, creating new nodes |
944 | +
// whenever one of b's original nodes would have been modified. Read operations |
945 | +
// should have no performance degredation. Write operations for both t and t2 |
946 | +
// will initially experience minor slow-downs caused by additional allocs and |
947 | +
// copies due to the aforementioned copy-on-write logic, but should converge to |
948 | +
// the original performance characteristics of the original tree. |
949 | +
func (t *BTree) Clone() (t2 *BTree) { |
950 | +
return (*BTree)((*BTreeG[Item])(t).Clone()) |
951 | +
} |
952 | +
|
953 | +
// Delete removes an item equal to the passed in item from the tree, returning |
954 | +
// it. If no such item exists, returns nil. |
955 | +
func (t *BTree) Delete(item Item) Item { |
956 | +
i, _ := (*BTreeG[Item])(t).Delete(item) |
957 | +
return i |
958 | +
} |
959 | +
|
960 | +
// DeleteMax removes the largest item in the tree and returns it. |
961 | +
// If no such item exists, returns nil. |
962 | +
func (t *BTree) DeleteMax() Item { |
963 | +
i, _ := (*BTreeG[Item])(t).DeleteMax() |
964 | +
return i |
965 | +
} |
966 | +
|
967 | +
// DeleteMin removes the smallest item in the tree and returns it. |
968 | +
// If no such item exists, returns nil. |
969 | +
func (t *BTree) DeleteMin() Item { |
970 | +
i, _ := (*BTreeG[Item])(t).DeleteMin() |
971 | +
return i |
972 | +
} |
973 | +
|
974 | +
// Get looks for the key item in the tree, returning it. It returns nil if |
975 | +
// unable to find that item. |
976 | +
func (t *BTree) Get(key Item) Item { |
977 | +
i, _ := (*BTreeG[Item])(t).Get(key) |
978 | +
return i |
979 | +
} |
980 | +
|
981 | +
// Max returns the largest item in the tree, or nil if the tree is empty. |
982 | +
func (t *BTree) Max() Item { |
983 | +
i, _ := (*BTreeG[Item])(t).Max() |
984 | +
return i |
985 | +
} |
986 | +
|
987 | +
// Min returns the smallest item in the tree, or nil if the tree is empty. |
988 | +
func (t *BTree) Min() Item { |
989 | +
i, _ := (*BTreeG[Item])(t).Min() |
990 | +
return i |
991 | +
} |
992 | +
|
993 | +
// Has returns true if the given key is in the tree. |
994 | +
func (t *BTree) Has(key Item) bool { |
995 | +
return (*BTreeG[Item])(t).Has(key) |
996 | +
} |
997 | +
|
998 | +
// ReplaceOrInsert adds the given item to the tree. If an item in the tree |
999 | +
// already equals the given one, it is removed from the tree and returned. |
1000 | +
// Otherwise, nil is returned. |
1001 | +
// |
1002 | +
// nil cannot be added to the tree (will panic). |
1003 | +
func (t *BTree) ReplaceOrInsert(item Item) Item { |
1004 | +
i, _ := (*BTreeG[Item])(t).ReplaceOrInsert(item) |
1005 | +
return i |
1006 | +
} |
1007 | +
|
1008 | +
// AscendRange calls the iterator for every value in the tree within the range |
1009 | +
// [greaterOrEqual, lessThan), until iterator returns false. |
1010 | +
func (t *BTree) AscendRange(greaterOrEqual, lessThan Item, iterator ItemIterator) { |
1011 | +
(*BTreeG[Item])(t).AscendRange(greaterOrEqual, lessThan, (ItemIteratorG[Item])(iterator)) |
1012 | +
} |
1013 | +
|
1014 | +
// AscendLessThan calls the iterator for every value in the tree within the range |
1015 | +
// [first, pivot), until iterator returns false. |
1016 | +
func (t *BTree) AscendLessThan(pivot Item, iterator ItemIterator) { |
1017 | +
(*BTreeG[Item])(t).AscendLessThan(pivot, (ItemIteratorG[Item])(iterator)) |
1018 | +
} |
1019 | +
|
1020 | +
// AscendGreaterOrEqual calls the iterator for every value in the tree within |
1021 | +
// the range [pivot, last], until iterator returns false. |
1022 | +
func (t *BTree) AscendGreaterOrEqual(pivot Item, iterator ItemIterator) { |
1023 | +
(*BTreeG[Item])(t).AscendGreaterOrEqual(pivot, (ItemIteratorG[Item])(iterator)) |
1024 | +
} |
1025 | +
|
1026 | +
// Ascend calls the iterator for every value in the tree within the range |
1027 | +
// [first, last], until iterator returns false. |
1028 | +
func (t *BTree) Ascend(iterator ItemIterator) { |
1029 | +
(*BTreeG[Item])(t).Ascend((ItemIteratorG[Item])(iterator)) |
1030 | +
} |
1031 | +
|
1032 | +
// DescendRange calls the iterator for every value in the tree within the range |
1033 | +
// [lessOrEqual, greaterThan), until iterator returns false. |
1034 | +
func (t *BTree) DescendRange(lessOrEqual, greaterThan Item, iterator ItemIterator) { |
1035 | +
(*BTreeG[Item])(t).DescendRange(lessOrEqual, greaterThan, (ItemIteratorG[Item])(iterator)) |
1036 | +
} |
1037 | +
|
1038 | +
// DescendLessOrEqual calls the iterator for every value in the tree within the range |
1039 | +
// [pivot, first], until iterator returns false. |
1040 | +
func (t *BTree) DescendLessOrEqual(pivot Item, iterator ItemIterator) { |
1041 | +
(*BTreeG[Item])(t).DescendLessOrEqual(pivot, (ItemIteratorG[Item])(iterator)) |
1042 | +
} |
1043 | +
|
1044 | +
// DescendGreaterThan calls the iterator for every value in the tree within |
1045 | +
// the range [last, pivot), until iterator returns false. |
1046 | +
func (t *BTree) DescendGreaterThan(pivot Item, iterator ItemIterator) { |
1047 | +
(*BTreeG[Item])(t).DescendGreaterThan(pivot, (ItemIteratorG[Item])(iterator)) |
1048 | +
} |
1049 | +
|
1050 | +
// Descend calls the iterator for every value in the tree within the range |
1051 | +
// [last, first], until iterator returns false. |
1052 | +
func (t *BTree) Descend(iterator ItemIterator) { |
1053 | +
(*BTreeG[Item])(t).Descend((ItemIteratorG[Item])(iterator)) |
1054 | +
} |
1055 | +
|
1056 | +
// Len returns the number of items currently in the tree. |
1057 | +
func (t *BTree) Len() int { |
1058 | +
return (*BTreeG[Item])(t).Len() |
1059 | +
} |
1060 | +
|
1061 | +
// Clear removes all items from the btree. If addNodesToFreelist is true, |
1062 | +
// t's nodes are added to its freelist as part of this call, until the freelist |
1063 | +
// is full. Otherwise, the root node is simply dereferenced and the subtree |
1064 | +
// left to Go's normal GC processes. |
1065 | +
// |
1066 | +
// This can be much faster |
1067 | +
// than calling Delete on all elements, because that requires finding/removing |
1068 | +
// each element in the tree and updating the tree accordingly. It also is |
1069 | +
// somewhat faster than creating a new tree to replace the old one, because |
1070 | +
// nodes from the old tree are reclaimed into the freelist for use by the new |
1071 | +
// one, instead of being lost to the garbage collector. |
1072 | +
// |
1073 | +
// This call takes: |
1074 | +
// O(1): when addNodesToFreelist is false, this is a single operation. |
1075 | +
// O(1): when the freelist is already full, it breaks out immediately |
1076 | +
// O(freelist size): when the freelist is empty and the nodes are all owned |
1077 | +
// by this tree, nodes are added to the freelist until full. |
1078 | +
// O(tree size): when all nodes are owned by another tree, all nodes are |
1079 | +
// iterated over looking for nodes to add to the freelist, and due to |
1080 | +
// ownership, none are. |
1081 | +
func (t *BTree) Clear(addNodesToFreelist bool) { |
1082 | +
(*BTreeG[Item])(t).Clear(addNodesToFreelist) |
1083 | +
} |
简述:
google实现的btree包,本身其实已经做了很多的优化,其中每个外部可以调用的函数都有对应的Benchmark,这对我们分析泛型带来的优势提供了很大的帮助.
本文不讨论btree本身的优劣,单纯的从一个可以进行数据操作的集合函数库来着重讨论泛型带来的golang性能优化.
如果对golang的1.18新带来的泛型(generics)不太了解,可以先阅读一下 https://go.dev/doc/tutorial/generics (官方) 或者 https://segmentfault.com/a/1190000041634906 (中文讲解)
实现解析:
- 如上文diff代码能看到的,泛型化的btree代码,主要就是将原来的
item
/node
替换成item[T]
/node[T]
@@ -72,32 +82,27 @@
-type FreeList struct {
+type FreeListG[T any] struct {
mu sync.Mutex
- freelist []*node
+ freelist []*node[T]
}
// size is the maximum size of the returned free list.
-func NewFreeList(size int) *FreeList {
- return &FreeList{freelist: make([]*node, 0, size)}
+func NewFreeListG[T any](size int) *FreeListG[T] {
+ return &FreeListG[T]{freelist: make([]*node[T], 0, size)}
}
-func (f *FreeList) newNode() (n *node) {
+func (f *FreeListG[T]) newNode() (n *node[T]) {
f.mu.Lock()
index := len(f.freelist) - 1
if index < 0 {
f.mu.Unlock()
- return new(node)
+ return new(node[T])
}
n = f.freelist[index]
f.freelist[index] = nil
这一块就是标准的泛型更新,不需要特别的说明.
- 全新定义了泛型里面可以被排序的类型集合与他们的generics的比较函数
// LessFunc[T] 函数是用来定义泛型的类型形参(Type parameter) 'T'是如何比较大小的.
// 返回的顺序必须是严密的,且当'a' < 'b'是返回true.
type LessFunc[T any] func(a, b T) bool
// Ordered 定义了golang原生可以被比较大小的内置类型
// @Note: 这里主要是做前向兼容,如果有自定义的类型,实现了`Less`方法也可以用
type Ordered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~float32 | ~float64 | ~string
}
// Less[T] 就是内置的Less函数,直接返回支持的泛型类型形参的比较结果
func Less[T Ordered]() LessFunc[T] {
return func(a, b T) bool { return a < b }
}
相对的,老版本直接是定义了一个Item
的interface{}
来实现同样的Less函数
// Item 表示树上的一个独立对象.
type Item interface {
// Less 函数必须要实现判断当前值是否小于传递进来的值.
// 相等的值只会保存一个
Less(than Item) bool
}
- 修改内部函数的返回值
@@ -308,70 +290,70 @@
-func (n *node) insert(item Item, maxItems int) Item {
- i, found := n.items.find(item)
+func (n *node[T]) insert(item T, maxItems int) (_ T, _ bool) {
+ i, found := n.items.find(item, n.cow.less)
if found {
out := n.items[i]
n.items[i] = item
- return out
+ return out, true
}
if len(n.children) == 0 {
n.items.insertAt(i, item)
- return nil
+ return
}
@@ -424,8 +406,9 @@
// 在这里使用的了一个内部的'remove'调用并且传递了一个'maxItem'参数,
// 拉取i的前一项(最右边的子节点)并设置到拉取的当前节点上
- n.items[i] = child.remove(nil, minItems, removeMax)
- return out
+ var zero T
+ n.items[i], _ = child.remove(zero, minItems, removeMax)
+ return out, true
}
之前的函数实现返回Item
,Item
是一个interface{}
(1.18之后是一个基本接口),之前可以通过直接返回一个nil
表示没有找到数据或者操作失败
但是泛型之后,不同的类型形参不能表示这种状态,所以在返回泛型的形参T
之外,还增加返回了一个bool
类型来表示操作结果
这里的Item
(实际是一个interface{}
接口)是性能瓶颈与优化的核心,后续我们回详细展开聊到
- 最后再增加亿点点兼容代码
// 前向兼容的方式,定义泛型下的'BTree'是一个'BTreeG'的Item约束
type BTree BTreeG[Item]
// 定义默认的函数,就是上文#2定义好的比较函数的实例
var itemLess LessFunc[Item] = func(a, b Item) bool {
return a.Less(b)
}
// New 创建一个新的'BTree',内部转换为创建一个'BTreeG[item]',同时传递less函数
func New(degree int) *BTree {
return (*BTree)(NewG[Item](degree, itemLess))
}
...更多的函数
性能测试结果
MR里面提到使用泛型改造之后,对于官方的bench函数(int存储),性能能优化40%+!
这里写一个简单的Bench来对比两种实现方式的性能
Code
package main
import (
"math/rand"
"testing"
"time"
"github.com/google/btree"
)
const treeSize = 50000
func init() {
rand.Seed(time.Now().Unix())
}
func BenchmarkBTree(b *testing.B) {
b.StopTimer()
insertP := rand.Perm(treeSize)
b.StartTimer()
i := 0
for i < b.N {
tr := btree.New(32)
for _, item := range insertP {
tr.ReplaceOrInsert(btree.Int(item))
i++
if i >= b.N {
return
}
}
}
}
func BenchmarkBTreeG(b *testing.B) {
b.StopTimer()
insertP := rand.Perm(treeSize)
b.StartTimer()
i := 0
for i < b.N {
tr := btree.NewOrderedG[int](32)
for _, item := range insertP {
tr.ReplaceOrInsert(item)
i++
if i >= b.N {
return
}
}
}
}
Bench执行之后的结果对比(使用github-codespace机器)
Running tool: /usr/local/go/bin/go test -benchmem -run=^$ -bench ^(BenchmarkBTree|BenchmarkBTreeG)$ gh/btree -count=1 -failfast
goos: linux
goarch: amd64
pkg: gh/btree
cpu: Intel(R) Xeon(R) Platinum 8272CL CPU @ 2.60GHz
BenchmarkBTree-4 3254396 373.8 ns/op 44 B/op 1 allocs/op
BenchmarkBTreeG-4 5487584 215.7 ns/op 19 B/op 0 allocs/op
PASS
ok gh/btree 5.106s
完整的官方全部bench对比结果:(再次感叹下原来版本的btree已经非常优化了,alloc(s)已经很小)
go test -bench -count=5 -failfast > [old|new].txt
benchstst old.txt new.txt
name | old time/op | new time/op | delta | |
---|---|---|---|---|
Insert-20 | 175ns ± 1% | 131ns ± 1% | -25.23% | (p=0.008 n=5+5) |
Seek-20 | 141ns ± 2% | 82ns ± 6% | -42.10% | (p=0.008 n=5+5) |
DeleteInsert-20 | 357ns ± 1% | 272ns ± 1% | -24.01% | (p=0.008 n=5+5) |
DeleteInsertCloneOnce-20 | 354ns ± 1% | 269ns ± 1% | -24.01% | (p=0.008 n=5+5) |
DeleteInsertCloneEachTime-20 | 1.53µs ± 1% | 0.97µs ± 7% | -36.70% | (p=0.008 n=5+5) |
Delete-20 | 196ns ± 1% | 145ns ± 2% | -25.91% | (p=0.008 n=5+5) |
Get-20 | 155ns ± 2% | 108ns ± 0% | -30.03% | (p=0.008 n=5+5) |
GetCloneEachTime-20 | 239ns ± 1% | 180ns ± 2% | -24.78% | (p=0.008 n=5+5) |
Ascend-20 | 47.7µs ± 1% | 35.0µs ± 1% | -26.63% | (p=0.008 n=5+5) |
Descend-20 | 47.3µs ± 1% | 34.1µs ± 1% | -27.88% | (p=0.008 n=5+5) |
AscendRange-20 | 91.7µs ± 2% | 54.7µs ± 1% | -40.36% | (p=0.008 n=5+5) |
DescendRange-20 | 130µs ± 5% | 72µs ± 1% | -44.78% | (p=0.008 n=5+5) |
AscendGreaterOrEqual-20 | 56.9µs ± 1% | 42.0µs ± 1% | -26.19% | (p=0.008 n=5+5) |
DescendLessOrEqual-20 | 94.0µs ± 2% | 57.4µs ± 1% | -38.89% | (p=0.008 n=5+5) |
DeleteAndRestore/CopyBigFreeList-20 | 5.08ms ± 1% | 3.60ms ± 0% | -29.02% | (p=0.016 n=5+4) |
DeleteAndRestore/Copy-20 | 5.41ms ± 3% | 3.66ms ± 0% | -32.37% | (p=0.008 n=5+5) |
DeleteAndRestore/ClearBigFreelist-20 | 2.92ms ± 1% | 2.16ms ± 1% | -26.09% | (p=0.008 n=5+5) |
DeleteAndRestore/Clear-20 | 3.14ms ± 1% | 2.24ms ± 1% | -28.91% | (p=0.008 n=5+5) |
name | old alloc/op | new alloc/op | delta | |
---|---|---|---|---|
Insert-20 | 36.0B ± 3% | 18.4B ± 3% | -48.89% | (p=0.008 n=5+5) |
Seek-20 | 7.00B ± 0% | 0.00B | -100.00% | (p=0.008 n=5+5) |
DeleteInsert-20 | 0.00B | 0.00B | ~ | (all equal) |
DeleteInsertCloneOnce-20 | 0.00B | 0.00B | ~ | (all equal) |
DeleteInsertCloneEachTime-20 | 2.97kB ± 1% | 1.95kB ± 4% | -34.30% | (p=0.008 n=5+5) |
Delete-20 | 0.00B | 0.00B | ~ | (all equal) |
Get-20 | 0.00B | 0.00B | ~ | (all equal) |
GetCloneEachTime-20 | 64.0B ± 0% | 64.0B ± 0% | ~ | (all equal) |
Ascend-20 | 0.00B | 0.00B | ~ | (all equal) |
Descend-20 | 0.00B | 0.00B | ~ | (all equal) |
AscendRange-20 | 0.00B | 0.00B | ~ | (all equal) |
DescendRange-20 | 0.00B | 0.00B | ~ | (all equal) |
AscendGreaterOrEqual-20 | 0.00B | 0.00B | ~ | (all equal) |
DescendLessOrEqual-20 | 0.00B | 0.00B | ~ | (all equal) |
DeleteAndRestore/CopyBigFreeList-20 | 274kB ± 0% | 142kB ± 0% | -48.20% | (p=0.008 n=5+5) |
DeleteAndRestore/Copy-20 | 876kB ± 0% | 443kB ± 0% | -49.36% | (p=0.008 n=5+5) |
DeleteAndRestore/ClearBigFreelist-20 | 631B ± 1% | 510B ± 5% | -19.06% | (p=0.008 n=5+5) |
DeleteAndRestore/Clear-20 | 553kB ± 0% | 278kB ± 0% | -49.77% | (p=0.000 n=5+4) |
name | old allocs/op | new allocs/op | delta | |
---|---|---|---|---|
Insert-20 | 0.00 | 0.00 | ~ | (all equal) |
Seek-20 | 0.00 | 0.00 | ~ | (all equal) |
DeleteInsert-20 | 0.00 | 0.00 | ~ | (all equal) |
DeleteInsertCloneOnce-20 | 0.00 | 0.00 | ~ | (all equal) |
DeleteInsertCloneEachTime-20 | 11.0 ± 0% | 11.0 ± 0% | ~ | (all equal) |
Delete-20 | 0.00 | 0.00 | ~ | (all equal) |
Get-20 | 0.00 | 0.00 | ~ | (all equal) |
GetCloneEachTime-20 | 3.00 ± 0% | 3.00 ± 0% | ~ | (all equal) |
Ascend-20 | 0.00 | 0.00 | ~ | (all equal) |
Descend-20 | 0.00 | 0.00 | ~ | (all equal) |
AscendRange-20 | 0.00 | 0.00 | ~ | (all equal) |
DescendRange-20 | 0.00 | 0.00 | ~ | (all equal) |
AscendGreaterOrEqual-20 | 0.00 | 0.00 | ~ | (all equal) |
DescendLessOrEqual-20 | 0.00 | 0.00 | ~ | (all equal) |
DeleteAndRestore/CopyBigFreeList-20 | 12.0 ± 0% | 14.0 ± 0% | +16.67% | (p=0.008 n=5+5) |
DeleteAndRestore/Copy-20 | 1.18k ± 0% | 1.13k ± 0% | -4.15% | (p=0.008 n=5+5) |
DeleteAndRestore/ClearBigFreelist-20 | 1.00 ± 0% | 1.00 ± 0% | ~ | (all equal) |
DeleteAndRestore/Clear-20 | 1.07k ± 0% | 1.02k ± 0% | -4.49% | (p=0.008 n=5+5) |
原理分析与说明
细心的人已经发现了端倪,Seek
的bench结果显示泛型之后的代码的alloc/op
直接将为了0
google/btree
的bench使用的是一个int类型,但是传递的都是一个Item接口.在bench的开头,创建了一个填充了随机数的int数组(同时转换为btree.Int
,一个Item
的实现).
type node struct {
items items
children children
cow *copyOnWriteContext
}
type items []Item
type Item interface {
Less(than Item) bool
}
type children []*node
当我们调用一个函数的时候,比如Seek
实际内部调用AscendGreaterOrEqual
实际内部去循环t.root.iterate()
bench代码模拟了一个大树的随机数据创建,const benchmarkTreeSize = 10000
个int已经在函数的堆(heap)上了,正常来说,我们调用函数传参都是简单的复制了指向这个int地址的一个指针而已
但是当btree传递数据参数变成Item
之后,情况就发生了变化,这个时候,传递的int
地址的指针,变成了一个Item
的intertface{}
的指针,这个时候参数就会逃逸到栈上(escape to heap
)
这种情况其实很常见,比如最常用到的printf
系列函数,他接收的永远是interface{}
作为参数,golang的编译器就会认为这类的函数,任意传递的v都会被转换成interface{}之后复制到栈上,再进行处理
这其实很好理解,v
(aka Item
in btree)可以是任何类型,传递形参为了保证安全,会复制到一个叫做interface{}
的万能圣杯里,再去传递
// fmt.Printf 接收任意个interface{}变量,所以在go编译阶段,会逃逸到栈heap上
func Printf(format string, a ...interface{}) (n int, err error) {}
// btree.AscendGreaterOrEqual 接收一个Item变量,实际还是一个实现了Less函数的interface{},同样会被编译器转换成interface{}的指针,这个时候就会逃逸到栈上
func (t *BTree) AscendGreaterOrEqual(pivot Item, iterator ItemIterator) {}
// 泛型实现,在编译阶段就会解析成实际的int类型,此时的pivot就是int,不会发生逃逸
func (t *BTreeG[T]) AscendGreaterOrEqual(pivot T, iterator ItemIteratorG[T]) {}
想要检测是否发生逃逸也很简单,直接使用golang自带的go build -gcflags="-m" .
golang这些年来做了很多编译的智能推断与优化,实际情况没有这么简单,举一个编译器可以优化的例子:
如果函数虽然接收的是一个interface{},但是实际内部只用了一种类型断言的时候,go编译器是不会发生逃逸到栈的处理的
func IsUIntStr(v interface{}) string { if vi, ok := v.(int); ok && vi > 0 { return "true" } return "false" }
在这种情况下,是不会发生额外的复制行为的
回到正题,btree
接收的类型是Item
(interface{}
),当有大量的Item对象调用的时候,需要复制到栈(heap
)的数据也会很大.
这种情况在bench里面更加明显,Item
实际内部只是一个int
的占用空间,但是发生函数调用时,需要生成一个指向这个int
的完整的interface{}
,每调用一个参数,增加的指针比实际int的内存占用空间还多的多
一个非空的
interface{}
至少包含
- 一个itab指针,指向一个对象的类型信息
- 一个unsafe.Pointer,指向实际的data地址
@see: https://github.com/golang/go/blob/master/src/reflect/value.go#L200
使用了 泛型之后,上述的函数在编译阶段就会被解析成:
func (t *BTreeG[int]) AscendGreaterOrEqual(pivot int, iterator ItemIteratorG[int]) {}
这样永远传递的都是指向int
实际地址的一个确定指针,指挥复制一次指针,在函数的堆内就能完成操作
大量的对象逃逸到栈(heap)的时候,由于heap是随着函数调用随机不连续的调用与复制(copy+alloc),进一步加大gc了的扫描与处理时间,导致最终的性能下降
关于为什么逃逸到堆(heap)之后的go性能会下降,和对应的cpu与alloc相关的详细说明,可以参考下面这些,这里就不再赘述了:
引申
从上面的分析可以看到,泛型在处理interface{}
的问题上提供了一个新的优化思路,在我目前使用的小工具合集的utils里面也有一定的借鉴思路
可以假设有这样一个非常常用的需求: 把一组数组去重,需要同时支持[]string
,[]int64
,[]float64
等等
以前的go代码只有有两种思路:
- 使用 // go generate 使用模板自动生成N个函数
但是这样会带来一个问题,不同函数的签名方法名肯定是不一样的,比如
UniqStringSlice
或者是UniqInt64Slice
,对于调用方来说维护或者查询需要的函数也不简单 - 使用
switch .(type)
断言,全部代码写到一个函数里面去 但是老的版本,只能使用一个interface{}去接收slice,好处是一个函数走天下,但是需要通过runtime
+reflect
的方式去判断具体传入的是怎么样interface{}=>[]interface{}{}=> reflect.Slice(i).Interface()
多种转换,性能必然下降
有了泛型(generics)之后,这类需求就非常容易了:(这也是在我的项目升级之后的utils库里)
func UniqSlice[T constraints.Ordered](list []T) []T {
target := make([]T, 0)
listMap := make(map[T]bool)
for _, v := range list {
if _, ok := listMap[v]; !ok {
listMap[v] = true
target = append(target, v)
}
}
return target
}
总结
看起来泛型的引入没有对google/btree
的代码结构发生大的变化,但是实际测试结果却让性能好了不少,可以预见到很多使用slice
+interface{}
的场景,更改为泛型(generics)之后都能带来代码量的减少和一定的性能的提升
后续希望可以看到更多的go代码使用泛型来进行优化,或者给出更多的解决实际问题的思路.