mirror of
https://github.com/semihalev/twig.git
synced 2026-03-14 22:05:46 +01:00
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:
parent
2bb3155fb2
commit
e226efbbfb
5 changed files with 109 additions and 19 deletions
|
|
@ -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
24
expr.go
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
6
node.go
6
node.go
|
|
@ -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
48
string_benchmark_test.go
Normal 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
42
utility.go
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue