diff --git a/benchmark/MEMORY_RESULTS.md b/benchmark/MEMORY_RESULTS.md index 969c811..16e98e5 100644 --- a/benchmark/MEMORY_RESULTS.md +++ b/benchmark/MEMORY_RESULTS.md @@ -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. diff --git a/expr.go b/expr.go index 647009d..45e84f6 100644 --- a/expr.go +++ b/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 } diff --git a/node.go b/node.go index d84e437..37b61e3 100644 --- a/node.go +++ b/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 } diff --git a/string_benchmark_test.go b/string_benchmark_test.go new file mode 100644 index 0000000..f3da0c2 --- /dev/null +++ b/string_benchmark_test.go @@ -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) + } +} diff --git a/utility.go b/utility.go new file mode 100644 index 0000000..b32d26b --- /dev/null +++ b/utility.go @@ -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 +} \ No newline at end of file