Skip to content

increase performance by migrating from bytes.Buffer to strings.Builder#512

Merged
mna merged 1 commit intoPuerkitoBio:masterfrom
myxzlpltk:master
Feb 28, 2025
Merged

increase performance by migrating from bytes.Buffer to strings.Builder#512
mna merged 1 commit intoPuerkitoBio:masterfrom
myxzlpltk:master

Conversation

@myxzlpltk
Copy link
Copy Markdown

No description provided.

@mna
Copy link
Copy Markdown
Member

mna commented Feb 28, 2025

Hello Saddam!

Thanks for your interest in goquery, the idea is interesting but I don't want to merge a change just for change's sake, do you have benchmark results (bytes.Buffer vs strings.Builder in the various methods where you made the change) that demonstrate a measurable improvement?

Thanks,
Martin

@myxzlpltk
Copy link
Copy Markdown
Author

myxzlpltk commented Feb 28, 2025

I add benchmark to this. Please check commit 2aacd3d
Here is the result

goos: windows
goarch: amd64
pkg: github.com/PuerkitoBio/goquery
cpu: AMD Ryzen 7 5800HS with Radeon Graphics        
BenchmarkStringBuilder
BenchmarkStringBuilder-16    	165627522	         7.623 ns/op
BenchmarkBytesBuffer
BenchmarkBytesBuffer-16      	120914390	        11.38 ns/op
PASS

Process finished with the exit code 0

Edit: wait let me adjust the benchmark to test the changed code

@mna
Copy link
Copy Markdown
Member

mna commented Feb 28, 2025

Sorry, I meant benchmarking the goquery methods affected by the change, not benchmarking raw strings.Builder vs bytes.Buffer. I know it can improve performance in some situations (I think the main advantage is preventing a copy of the buffer when getting the resulting String()), but it seems marginal, I want to make sure it's worth the change in the context of the goquery methods.

I'll try to get some results on my side, by running the relevant benchmarks of the current version vs your changes.

@mna
Copy link
Copy Markdown
Member

mna commented Feb 28, 2025

Some results on my old Thinkpad T460:

# Buffer
cpu: Intel(R) Core(TM) i7-6600U CPU @ 2.60GHz
BenchmarkMetalReviewExample-4   	   10969	     95452 ns/op	    8418 B/op	     343 allocs/op
# Builder
BenchmarkMetalReviewExample-4   	   12570	     97463 ns/op	    6570 B/op	     313 allocs/op
PASS
#Buffer
BenchmarkHtml-4   	 4094517	       299.5 ns/op	     120 B/op	       3 allocs/op
PASS
# Builder
BenchmarkHtml-4   	 5578891	       230.6 ns/op	      40 B/op	       2 allocs/op
PASS

A bit slower but less memory usage in the first benchmark, but it does seem like a bit of an improvement in the second. Unfortunately I don't have an existing benchmark for OuterHtml. If you can remove the latest commit (with the Builder/Buffer benchmark), I'd be happy to merge!

Thanks,
Martin

@myxzlpltk
Copy link
Copy Markdown
Author

Done

@mna
Copy link
Copy Markdown
Member

mna commented Feb 28, 2025

One more:

# Buffer
BenchmarkText-4   	  605391	      1995 ns/op	     704 B/op	       4 allocs/op
PASS
# Builder
BenchmarkText-4   	  591198	      2016 ns/op	     504 B/op	       6 allocs/op
PASS

Again, ever so slightly slower but less memory usage.

@mna mna merged commit c2d1a09 into PuerkitoBio:master Feb 28, 2025
@mna
Copy link
Copy Markdown
Member

mna commented Feb 28, 2025

Thanks for your contribution!

@myxzlpltk
Copy link
Copy Markdown
Author

I got different result, hmmm

goos: windows
goarch: amd64
pkg: github.com/PuerkitoBio/goquery
cpu: AMD Ryzen 7 5800HS with Radeon Graphics        
BenchmarkStringsBuilderHtml
BenchmarkStringsBuilderHtml-16         	1000000000	         0.0000539 ns/op	       0 B/op	       0 allocs/op
BenchmarkBytesBufferHtml
BenchmarkBytesBufferHtml-16            	1000000000	         0.0000686 ns/op	       0 B/op	       0 allocs/op
BenchmarkStringsBuilderText
BenchmarkStringsBuilderText-16         	1000000000	         0.0000215 ns/op	       0 B/op	       0 allocs/op
BenchmarkBytesBufferText
BenchmarkBytesBufferText-16            	1000000000	         0.0000269 ns/op	       0 B/op	       0 allocs/op
BenchmarkStringsBuilderOuterHtml
BenchmarkStringsBuilderOuterHtml-16    	1000000000	         0.0000528 ns/op	       0 B/op	       0 allocs/op
BenchmarkBytesBufferOuterHtml
BenchmarkBytesBufferOuterHtml-16       	1000000000	         0.0000706 ns/op	       0 B/op	       0 allocs/op
PASS

Process finished with the exit code 0
package goquery

import (
	"bytes"
	"golang.org/x/net/html"
	"strings"
	"testing"
)

func BenchmarkStringsBuilderHtml(b *testing.B) {
	// Find selector and reset timer
	s := DocB().Find("body")
	b.ResetTimer()
	// Since there is no .innerHtml, the HTML content must be re-created from
	// the nodes using html.Render.
	var builder strings.Builder

	if len(s.Nodes) > 0 {
		for c := s.Nodes[0].FirstChild; c != nil; c = c.NextSibling {
			err := html.Render(&builder, c)
			if err != nil {
				b.Fatal("Unexpected error:", err)
			}
		}
		_ = builder.String()
	}
}

func BenchmarkBytesBufferHtml(b *testing.B) {
	// Find selector and reset timer
	s := DocB().Find("body")
	b.ResetTimer()
	// Since there is no .innerHtml, the HTML content must be re-created from
	// the nodes using html.Render.
	var buf bytes.Buffer

	if len(s.Nodes) > 0 {
		for c := s.Nodes[0].FirstChild; c != nil; c = c.NextSibling {
			err := html.Render(&buf, c)
			if err != nil {
				b.Fatal("Unexpected error:", err)
			}
		}
		_ = buf.String()
	}
}

func BenchmarkStringsBuilderText(b *testing.B) {
	// Find selector and reset timer
	s := DocB().Find("body")
	b.ResetTimer()

	var builder strings.Builder

	// Slightly optimized vs calling Each: no single selection object created
	var f func(*html.Node)
	f = func(n *html.Node) {
		if n.Type == html.TextNode {
			// Keep newlines and spaces, like jQuery
			builder.WriteString(n.Data)
		}
		if n.FirstChild != nil {
			for c := n.FirstChild; c != nil; c = c.NextSibling {
				f(c)
			}
		}
	}
	for _, n := range s.Nodes {
		f(n)
	}

	_ = builder.String()
}

func BenchmarkBytesBufferText(b *testing.B) {
	// Find selector and reset timer
	s := DocB().Find("body")
	b.ResetTimer()

	var buf bytes.Buffer

	// Slightly optimized vs calling Each: no single selection object created
	var f func(*html.Node)
	f = func(n *html.Node) {
		if n.Type == html.TextNode {
			// Keep newlines and spaces, like jQuery
			buf.WriteString(n.Data)
		}
		if n.FirstChild != nil {
			for c := n.FirstChild; c != nil; c = c.NextSibling {
				f(c)
			}
		}
	}
	for _, n := range s.Nodes {
		f(n)
	}

	_ = buf.String()
}

func BenchmarkStringsBuilderOuterHtml(b *testing.B) {
	// Find selector and reset timer
	s := DocB().Find("body")
	b.ResetTimer()

	var builder strings.Builder

	if err := Render(&builder, s); err != nil {
		b.Fatal("Unexpected error:", err)
	}

	_ = builder.String()
}

func BenchmarkBytesBufferOuterHtml(b *testing.B) {
	// Find selector and reset timer
	s := DocB().Find("body")
	b.ResetTimer()

	var buf bytes.Buffer

	if err := Render(&buf, s); err != nil {
		b.Fatal("Unexpected error:", err)
	}
	_ = buf.String()
}

@mna
Copy link
Copy Markdown
Member

mna commented Feb 28, 2025

With those fractional nanosecond times, the compiler is probably optimizing your benchmark away. I already have benchmarks for Html, Text and the example, so I ran them before and after your changes.

@mna
Copy link
Copy Markdown
Member

mna commented Feb 28, 2025

On closer look you're missing the for range b.N { ... } benchmark loop, which explains the times.

@myxzlpltk
Copy link
Copy Markdown
Author

ahh i see, now its working

goos: windows
goarch: amd64
pkg: github.com/PuerkitoBio/goquery
cpu: AMD Ryzen 7 5800HS with Radeon Graphics        
BenchmarkStringsBuilderHtml
BenchmarkStringsBuilderHtml-16         	   21266	     53738 ns/op	   84760 B/op	      19 allocs/op
BenchmarkBytesBufferHtml
BenchmarkBytesBufferHtml-16            	   23674	     50907 ns/op	   87280 B/op	      12 allocs/op
BenchmarkStringsBuilderText
BenchmarkStringsBuilderText-16         	   80449	     15603 ns/op	   62968 B/op	      17 allocs/op
BenchmarkBytesBufferText
BenchmarkBytesBufferText-16            	   93949	     12658 ns/op	   46272 B/op	      10 allocs/op
BenchmarkStringsBuilderOuterHtml
BenchmarkStringsBuilderOuterHtml-16    	   22291	     54039 ns/op	   84760 B/op	      19 allocs/op
BenchmarkBytesBufferOuterHtml
BenchmarkBytesBufferOuterHtml-16       	   23346	     52020 ns/op	   87280 B/op	      12 allocs/op
PASS

@mna
Copy link
Copy Markdown
Member

mna commented Feb 28, 2025

Yeah not an undisputable improvement for sure, but it feels cleaner since those are all building strings.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants