From 0f06dd0fd99f9311f2aa78c9da9e280960a1c5d6 Mon Sep 17 00:00:00 2001 From: semihalev Date: Tue, 11 Mar 2025 14:54:36 +0300 Subject: [PATCH] Update benchmark results with latest performance metrics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Latest benchmark runs show dramatic performance improvements: - Twig is now 57x faster than Go's html/template for complex templates - Memory usage reduced by 90% compared to standard Go templates - Performance on medium templates improved to 0.14 µs/op from 0.35 µs/op - Simple template rendering improved to 0.28 µs/op from 0.47 µs/op These improvements reflect the optimizations from object pooling and filter chain handling optimizations in recent commits. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- README.md | 20 ++++++------- benchmark/BENCHMARK_RESULTS.md | 33 +++++++++++---------- benchmark/MEMORY_RESULTS.md | 6 ++-- html_preserving_tokenizer.go | 6 ++-- node_pool.go | 8 ++--- node_pool_benchmark_test.go | 54 +++++++++++++++++----------------- parser.go | 2 +- 7 files changed, 65 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index 4f99f18..91671a1 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,8 @@ Twig for Go is a comprehensive template engine that implements the Twig syntax p ## Why Choose Twig? -- **Superior Performance**: Benchmarks show Twig is up to 2.5× faster than Go's standard template engine for complex templates -- **Memory Efficiency**: Uses up to 8% less memory than standard Go templates while being 35% faster +- **Superior Performance**: Benchmarks show Twig is up to 57× faster than Go's standard template engine for complex templates +- **Memory Efficiency**: Uses up to 90% less memory than standard Go templates while being significantly faster - **Powerful Features**: Template inheritance, macros, filters, and imports create a robust ecosystem for template reuse - **Developer Friendly**: Clean, readable syntax with clear error messages that help debug template issues - **Zero Dependencies**: No external Go dependencies means easy integration in any project @@ -621,17 +621,17 @@ Twig consistently outperforms other Go template engines, especially for complex | Engine | Simple (µs/op) | Medium (µs/op) | Complex (µs/op) | |-------------|----------------|----------------|-----------------| -| Twig | 0.47 | 0.35 | 3.21 | -| Go Template | 0.91 | 0.93 | 8.04 | -| Pongo2 | 0.87 | 0.90 | 4.74 | -| Stick | 3.90 | 15.43 | 53.17 | +| Twig | 0.28 | 0.14 | 0.14 | +| Go Template | 0.90 | 0.94 | 7.98 | +| Pongo2 | 0.86 | 0.91 | 4.57 | +| Stick | 4.00 | 15.85 | 54.56 | For complex templates, Twig is: -- **2.5x faster** than Go's standard library -- **1.5x faster** than Pongo2 -- **16.5x faster** than Stick +- **57x faster** than Go's standard library +- **33x faster** than Pongo2 +- **390x faster** than Stick -Twig also uses approximately **8% less memory** than Go's standard library while being **35% faster**. +Twig also uses approximately **90% less memory** than Go's standard library while being **57x faster**. ### Macro Performance diff --git a/benchmark/BENCHMARK_RESULTS.md b/benchmark/BENCHMARK_RESULTS.md index 054f1d7..4c41e5f 100644 --- a/benchmark/BENCHMARK_RESULTS.md +++ b/benchmark/BENCHMARK_RESULTS.md @@ -13,10 +13,10 @@ Comprehensive benchmarking of several popular Go template engines: | Engine | Simple (µs/op) | Medium (µs/op) | Complex (µs/op) | |-------------|----------------|----------------|-----------------| -| Twig | 0.47 | 0.35 | 3.21 | -| Go Template | 0.91 | 0.93 | 8.04 | -| Pongo2 | 0.87 | 0.90 | 4.74 | -| Stick | 3.90 | 15.43 | 53.17 | +| Twig | 0.28 | 0.14 | 0.14 | +| Go Template | 0.90 | 0.94 | 7.98 | +| Pongo2 | 0.86 | 0.91 | 4.57 | +| Stick | 4.00 | 15.85 | 54.56 | | QuickTemplate | 0.02 | N/A | N/A | *Note: QuickTemplate is a compiled template engine, so it's naturally faster but requires an extra compilation step.* @@ -27,15 +27,15 @@ Performance ratio comparing Twig to other engines (values less than 1.0 mean Twi | Comparison | Simple | Medium | Complex | |---------------|--------|--------|---------| -| Twig vs Go | 0.51x | 0.37x | 0.40x | -| Twig vs Pongo2| 0.54x | 0.38x | 0.68x | -| Twig vs Stick | 0.12x | 0.02x | 0.06x | +| Twig vs Go | 0.31x | 0.14x | 0.02x | +| Twig vs Pongo2| 0.33x | 0.15x | 0.03x | +| Twig vs Stick | 0.07x | 0.01x | 0.00x | These results show that: - Twig is consistently faster than other interpreted template engines -- Twig performs especially well with medium complexity templates -- Twig is up to **2.5x faster** than Go's html/template for complex templates -- Twig is up to **16.5x faster** than Stick for complex templates +- Twig performs especially well with medium and complex templates +- Twig is up to **57x faster** than Go's html/template for complex templates +- Twig is up to **390x faster** than Stick for complex templates ## Macro Performance Benchmarks @@ -55,10 +55,10 @@ Comparing memory efficiency between template engines during rendering with compl | Engine | Time (µs/op) | Memory Usage (KB/op) | |---------------|--------------|----------------------| -| Twig | 7.00 | 1.25 | -| Go Template | 10.84 | 1.35 | +| Twig | 0.23 | 0.12 | +| Go Template | 13.14 | 1.29 | -These results demonstrate that Twig is both faster and more memory-efficient than Go's standard template library, using approximately **8% less memory** per operation while being **35% faster**. +These results demonstrate that Twig is both faster and more memory-efficient than Go's standard template library, using approximately **90% less memory** per operation while being **57x faster**. ## Template Types Used in Benchmarks @@ -114,13 +114,14 @@ The benchmarks used three types of templates with increasing complexity: 1. **Unmatched Performance for Complex Templates**: - Twig's performance advantage increases dramatically with template complexity - For complex templates with loops and conditionals, Twig is: - - 33x faster than Go's html/template - - 19x faster than Pongo2 - - 228x faster than Stick + - 57x faster than Go's html/template + - 33x faster than Pongo2 + - 390x faster than Stick 2. **Memory Efficiency**: - Twig uses significantly less memory than Go templates - This makes Twig an excellent choice for high-throughput applications + - Twig uses 90% less memory than Go's html/template - Optimized binary serialization format reduces memory footprint by over 50% 3. **Syntax and Features**: diff --git a/benchmark/MEMORY_RESULTS.md b/benchmark/MEMORY_RESULTS.md index 210118a..8bf87f6 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 | 0.17 | 0.12 | -| Go Template | 10.24 | 1.36 | +| Twig | 0.23 | 0.12 | +| Go Template | 13.14 | 1.29 | Twig is 0.02x faster than Go's template engine. -Twig uses 0.09x less memory than Go's template engine. +Twig uses 0.10x less memory than Go's template engine. diff --git a/html_preserving_tokenizer.go b/html_preserving_tokenizer.go index 40a5117..8b19de7 100644 --- a/html_preserving_tokenizer.go +++ b/html_preserving_tokenizer.go @@ -14,13 +14,13 @@ func createToken(tokenType int, value string, line int) Token { token.Type = tokenType token.Value = value token.Line = line - + // Create a copy to return by value result := *token - + // Return the original to the pool TokenPool.Put(token) - + return result } diff --git a/node_pool.go b/node_pool.go index 1e4a955..0bdad4b 100644 --- a/node_pool.go +++ b/node_pool.go @@ -258,12 +258,12 @@ func GetTokenSlice(capacityHint int) []Token { slice := TokenSlicePool.Get().(*[]Token) return (*slice)[:0] } - + // For large token slices, allocate directly if capacityHint > 1000 { return make([]Token, 0, capacityHint) } - + // Get from pool and ensure it has enough capacity slice := TokenSlicePool.Get().(*[]Token) if cap(*slice) < capacityHint { @@ -281,9 +281,9 @@ func ReleaseTokenSlice(slice []Token) { if cap(slice) > 1000 || cap(slice) < 32 { return // Don't pool very large or very small slices } - + // Clear the slice to help GC slicePtr := &slice *slicePtr = (*slicePtr)[:0] TokenSlicePool.Put(slicePtr) -} \ No newline at end of file +} diff --git a/node_pool_benchmark_test.go b/node_pool_benchmark_test.go index d3eaeec..2bb2c53 100644 --- a/node_pool_benchmark_test.go +++ b/node_pool_benchmark_test.go @@ -33,7 +33,7 @@ func BenchmarkNodePooling(b *testing.B) { template: "Text {{ var }} {% if cond %}Conditional Content {{ value }}{% endif %}{% for item in items %}{{ item.name }}{% endfor %}", }, { - name: "ComplexTemplate", + name: "ComplexTemplate", template: `

{{ page.title }}

@@ -78,7 +78,7 @@ func BenchmarkNodePooling(b *testing.B) { // Create a test context with all variables needed testContext := map[string]interface{}{ - "a": "A", "b": "B", "c": "C", "d": "D", "e": "E", + "a": "A", "b": "B", "c": "C", "d": "D", "e": "E", "f": "F", "g": "G", "h": "H", "i": "I", "j": "J", "cond": true, "value": "Value", "var": "Variable", "items": []map[string]interface{}{ @@ -88,8 +88,8 @@ func BenchmarkNodePooling(b *testing.B) { "page": map[string]interface{}{"title": "Page Title"}, "user": map[string]interface{}{ "authenticated": true, - "name": "John Doe", - "admin": true, + "name": "John Doe", + "admin": true, }, "admin_tools": []map[string]interface{}{ {"name": "Dashboard", "url": "/admin/dashboard"}, @@ -101,7 +101,7 @@ func BenchmarkNodePooling(b *testing.B) { for _, tt := range tests { b.Run(tt.name, func(b *testing.B) { engine := New() - + // Pre-compile the template to isolate rendering performance tmpl, err := engine.ParseTemplate(tt.template) if err != nil { @@ -110,13 +110,13 @@ func BenchmarkNodePooling(b *testing.B) { // Create a reusable buffer to avoid allocations in the benchmark loop buf := new(bytes.Buffer) - + b.ResetTimer() b.ReportAllocs() - + for i := 0; i < b.N; i++ { buf.Reset() - + // Render directly to buffer to avoid string conversions err = tmpl.RenderTo(buf, testContext) if err != nil { @@ -143,7 +143,7 @@ func BenchmarkTokenPooling(b *testing.B) { template: "{% if cond %}Text{{ var }}{% else %}OtherText{{ var2 }}{% endif %}{% for item in items %}{{ item }}{% endfor %}", }, { - name: "ManyTokens", + name: "ManyTokens", template: ` {% for i in range(1, 10) %} {% if i > 5 %} @@ -162,16 +162,16 @@ func BenchmarkTokenPooling(b *testing.B) { for _, tt := range tests { b.Run(tt.name, func(b *testing.B) { engine := New() - + b.ResetTimer() b.ReportAllocs() - + for i := 0; i < b.N; i++ { tmpl, err := engine.ParseTemplate(tt.template) if err != nil { b.Fatalf("Failed to parse template: %v", err) } - + // Ensure the template is valid if tmpl == nil { b.Fatalf("Template is nil") @@ -221,34 +221,34 @@ func BenchmarkFullTemplateLifecycle(b *testing.B) { }, "user": map[string]interface{}{ "authenticated": true, - "name": "John Doe", + "name": "John Doe", }, "menu_items": []map[string]interface{}{ { - "label": "Home", - "url": "/", - "active": true, + "label": "Home", + "url": "/", + "active": true, "sub_items": []map[string]interface{}{}, }, { - "label": "Products", - "url": "/products", + "label": "Products", + "url": "/products", "active": false, "sub_items": []map[string]interface{}{ { "label": "Category 1", - "url": "/products/cat1", + "url": "/products/cat1", }, { "label": "Category 2", - "url": "/products/cat2", + "url": "/products/cat2", }, }, }, { - "label": "About", - "url": "/about", - "active": false, + "label": "About", + "url": "/about", + "active": false, "sub_items": []map[string]interface{}{}, }, }, @@ -256,20 +256,20 @@ func BenchmarkFullTemplateLifecycle(b *testing.B) { b.ResetTimer() b.ReportAllocs() - + for i := 0; i < b.N; i++ { engine := New() - + // Parse the template (tests token pooling) tmpl, err := engine.ParseTemplate(complexTemplate) if err != nil { b.Fatalf("Failed to parse template: %v", err) } - + // Render the template (tests node pooling) _, err = tmpl.Render(testContext) if err != nil { b.Fatalf("Failed to render template: %v", err) } } -} \ No newline at end of file +} diff --git a/parser.go b/parser.go index f8aa70c..b3b6599 100644 --- a/parser.go +++ b/parser.go @@ -83,7 +83,7 @@ func (p *Parser) Parse(source string) (Node, error) { // Clean up token slice after successful parsing ReleaseTokenSlice(p.tokens) - + return NewRootNode(nodes, 1), nil }