perf: presize the result slice in Empty()

Empty() collected the removed children into a nil slice grown via append,
forcing a series of grow-and-double reallocations proportional to the number
of children.

This commit walks the children once up front to count them, then allocate the
result slice at its final capacity before the removal loop.

The count pass is a cheap, branch-predicted pointer walk that replaces several
slice reallocations with a single allocation.

benchstat (-benchtime=2000x, count=10):

                |   old    |            new            |
                |  allocs  |  allocs    vs base        |
    Empty10-8   |   3.000  |  2.000     -33.33%        |
    Empty100-8  |   6.000  |  2.000     -66.67%        |
    Empty1000-8 |   9.000  |  2.000     -77.78%        |

    B/op:    -47% / -56% / -53%
    sec/op:  Empty10 -53%, Empty100 -58%, Empty1000 ~ (noise)
This commit is contained in:
jvoisin
2026-06-01 01:23:40 +02:00
parent fa0370e85c
commit ea09fe9e1c
+9 -1
View File
@@ -166,8 +166,16 @@ func (s *Selection) Clone() *Selection {
// Empty removes all children nodes from the set of matched elements.
// It returns the children nodes in a new Selection.
func (s *Selection) Empty() *Selection {
var nodes []*html.Node
// Count the children first so nodes can be presized, avoiding append's
// repeated reallocations.
count := 0
for _, n := range s.Nodes {
for c := n.FirstChild; c != nil; c = c.NextSibling {
count++
}
}
nodes := make([]*html.Node, 0, count)
for _, n := range s.Nodes {
for c := n.FirstChild; c != nil; c = n.FirstChild {
n.RemoveChild(c)