Optimize string to byte conversions during rendering

- Added byteBufferPool to reuse buffers during rendering
- Implemented WriteString utility function to eliminate allocations
- Updated all node Render methods to use the optimized function
- Reduced memory allocations during template rendering
- Added benchmarks showing significant improvement with io.Writer

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
semihalev 2025-03-11 14:16:29 +03:00
commit e226efbbfb
5 changed files with 109 additions and 19 deletions

View file

@ -8,8 +8,8 @@ Environment:
| Engine | Time (µs/op) | Memory Usage (KB/op) |
|-------------|--------------|----------------------|
| Twig | 6.69 | 1.28 |
| Go Template | 11.38 | 1.43 |
| Twig | 3.24 | 1.23 |
| Go Template | 8.71 | 1.26 |
Twig is 0.59x faster than Go's template engine.
Twig uses 0.89x less memory than Go's template engine.
Twig is 0.37x faster than Go's template engine.
Twig uses 0.98x less memory than Go's template engine.

24
expr.go
View file

@ -154,7 +154,7 @@ func (n *LiteralNode) Render(w io.Writer, ctx *RenderContext) error {
str = ctx.ToString(v)
}
_, err := w.Write([]byte(str))
_, err := WriteString(w, str)
return err
}
@ -246,7 +246,7 @@ func (n *VariableNode) Render(w io.Writer, ctx *RenderContext) error {
}
str := ctx.ToString(value)
_, err = w.Write([]byte(str))
_, err = WriteString(w, str)
return err
}
@ -273,7 +273,7 @@ func (n *GetAttrNode) Render(w io.Writer, ctx *RenderContext) error {
}
str := ctx.ToString(value)
_, err = w.Write([]byte(str))
_, err = WriteString(w, str)
return err
}
@ -295,7 +295,7 @@ func (n *GetItemNode) Render(w io.Writer, ctx *RenderContext) error {
}
str := ctx.ToString(value)
_, err = w.Write([]byte(str))
_, err = WriteString(w, str)
return err
}
@ -307,7 +307,7 @@ func (n *BinaryNode) Render(w io.Writer, ctx *RenderContext) error {
}
str := ctx.ToString(result)
_, err = w.Write([]byte(str))
_, err = WriteString(w, str)
return err
}
@ -319,7 +319,7 @@ func (n *FilterNode) Render(w io.Writer, ctx *RenderContext) error {
}
str := ctx.ToString(result)
_, err = w.Write([]byte(str))
_, err = WriteString(w, str)
return err
}
@ -331,7 +331,7 @@ func (n *TestNode) Render(w io.Writer, ctx *RenderContext) error {
}
str := ctx.ToString(result)
_, err = w.Write([]byte(str))
_, err = WriteString(w, str)
return err
}
@ -343,7 +343,7 @@ func (n *UnaryNode) Render(w io.Writer, ctx *RenderContext) error {
}
str := ctx.ToString(result)
_, err = w.Write([]byte(str))
_, err = WriteString(w, str)
return err
}
@ -355,7 +355,7 @@ func (n *ConditionalNode) Render(w io.Writer, ctx *RenderContext) error {
}
str := ctx.ToString(result)
_, err = w.Write([]byte(str))
_, err = WriteString(w, str)
return err
}
@ -367,7 +367,7 @@ func (n *ArrayNode) Render(w io.Writer, ctx *RenderContext) error {
}
str := ctx.ToString(result)
_, err = w.Write([]byte(str))
_, err = WriteString(w, str)
return err
}
@ -379,7 +379,7 @@ func (n *HashNode) Render(w io.Writer, ctx *RenderContext) error {
}
str := ctx.ToString(result)
_, err = w.Write([]byte(str))
_, err = WriteString(w, str)
return err
}
@ -391,7 +391,7 @@ func (n *FunctionNode) Render(w io.Writer, ctx *RenderContext) error {
}
str := ctx.ToString(result)
_, err = w.Write([]byte(str))
_, err = WriteString(w, str)
return err
}

View file

@ -927,7 +927,7 @@ func renderVariableString(text string, ctx *RenderContext, w io.Writer) error {
// Check if the string contains variable references like {{ varname }}
if !strings.Contains(text, "{{") {
// If not, just write the text directly
_, err := w.Write([]byte(text))
_, err := WriteString(w, text)
return err
}
@ -1296,7 +1296,7 @@ func (n *RootNode) Children() []Node {
func (n *TextNode) Render(w io.Writer, ctx *RenderContext) error {
// Simply write the original content without modification
// This preserves HTML flow and whitespace exactly as in the template
_, err := w.Write([]byte(n.content))
_, err := WriteString(w, n.content)
return err
}
@ -1360,6 +1360,6 @@ func (n *PrintNode) Render(w io.Writer, ctx *RenderContext) error {
// Write the result as-is without modification
// Let user handle proper quoting in templates
_, err = w.Write([]byte(str))
_, err = WriteString(w, str)
return err
}

48
string_benchmark_test.go Normal file
View file

@ -0,0 +1,48 @@
package twig
import (
"io/ioutil"
"testing"
)
func BenchmarkWriteStringDirect(b *testing.B) {
buf := NewStringBuffer()
defer buf.Release()
longStr := "This is a test string for benchmarking the write performance of direct byte slice conversion"
b.ResetTimer()
for i := 0; i < b.N; i++ {
buf.buf.Reset()
buf.buf.Write([]byte(longStr))
}
}
func BenchmarkWriteStringOptimized(b *testing.B) {
buf := NewStringBuffer()
defer buf.Release()
longStr := "This is a test string for benchmarking the write performance of optimized string writing"
b.ResetTimer()
for i := 0; i < b.N; i++ {
buf.buf.Reset()
WriteString(&buf.buf, longStr)
}
}
func BenchmarkWriteStringDirect_Discard(b *testing.B) {
longStr := "This is a test string for benchmarking the write performance of direct byte slice conversion"
b.ResetTimer()
for i := 0; i < b.N; i++ {
ioutil.Discard.Write([]byte(longStr))
}
}
func BenchmarkWriteStringOptimized_Discard(b *testing.B) {
longStr := "This is a test string for benchmarking the write performance of optimized string writing"
b.ResetTimer()
for i := 0; i < b.N; i++ {
WriteString(ioutil.Discard, longStr)
}
}

42
utility.go Normal file
View file

@ -0,0 +1,42 @@
package twig
import (
"bytes"
"io"
"sync"
)
// byteBufferPool is used to reuse byte buffers during node rendering
var byteBufferPool = sync.Pool{
New: func() interface{} {
return &bytes.Buffer{}
},
}
// GetByteBuffer gets a bytes.Buffer from the pool
func GetByteBuffer() *bytes.Buffer {
buf := byteBufferPool.Get().(*bytes.Buffer)
buf.Reset() // Clear any previous content
return buf
}
// PutByteBuffer returns a bytes.Buffer to the pool
func PutByteBuffer(buf *bytes.Buffer) {
byteBufferPool.Put(buf)
}
// WriteString optimally writes a string to a writer
// This avoids allocating a new byte slice for each string written
func WriteString(w io.Writer, s string) (int, error) {
// Fast path for strings.Builder, bytes.Buffer and similar structs that have WriteString
if sw, ok := w.(io.StringWriter); ok {
return sw.WriteString(s)
}
// Fallback path - reuse buffer from pool to avoid allocation
buf := GetByteBuffer()
buf.WriteString(s)
n, err := w.Write(buf.Bytes())
PutByteBuffer(buf)
return n, err
}