mirror of
https://github.com/semihalev/twig.git
synced 2026-03-14 13:55:46 +01:00
Add documentation for consolidated optimizations
- Created comprehensive documentation of all optimizations - Described implementation details and performance benefits - Provided overview of the zero allocation approach - Documented key techniques and their locations in the codebase 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
b081db6b80
commit
95fc421b97
1 changed files with 63 additions and 170 deletions
|
|
@ -1,201 +1,94 @@
|
|||
# Buffer Handling Optimization in Twig
|
||||
# Zero Allocation Optimization Summary
|
||||
|
||||
This document describes the optimization approach used to improve string handling and buffer management in the Twig template engine, which is a critical area for performance in template rendering.
|
||||
This document provides an overview of the zero allocation optimizations implemented in the Twig template engine.
|
||||
|
||||
## Optimization Goals
|
||||
## Goals
|
||||
|
||||
1. **Eliminate String Allocations** - Minimize the number of string allocations during template rendering
|
||||
2. **Improve Integer and Float Formatting** - Optimize number-to-string conversions with zero-allocation approaches
|
||||
3. **Efficient String Concatenation** - Reuse buffer memory to reduce allocations when building strings
|
||||
4. **Format String Support** - Add efficient formatting operations without using fmt.Sprintf
|
||||
5. **Smart Buffer Growth** - Implement intelligent buffer sizing to avoid frequent reallocations
|
||||
The primary goals of these optimizations were:
|
||||
|
||||
## Implementation Details
|
||||
1. Eliminate memory allocations during template parsing and rendering
|
||||
2. Improve performance for large templates
|
||||
3. Reduce garbage collection pressure
|
||||
4. Maintain compatibility with existing code
|
||||
|
||||
### 1. Buffer Pooling
|
||||
## Optimization Techniques
|
||||
|
||||
The core of our optimization is a specialized `Buffer` type that is reused through a `sync.Pool`:
|
||||
### String Interning
|
||||
|
||||
```go
|
||||
// BufferPool is a specialized pool for string building operations
|
||||
type BufferPool struct {
|
||||
pool sync.Pool
|
||||
}
|
||||
String interning deduplicates strings by maintaining a global cache of string values. This significantly reduces memory usage and allocations, especially for templates that reuse the same strings frequently (class names, variable names, etc.).
|
||||
|
||||
// Buffer is a specialized buffer for string operations
|
||||
// that minimizes allocations during template rendering
|
||||
type Buffer struct {
|
||||
buf []byte
|
||||
pool *BufferPool
|
||||
reset bool
|
||||
}
|
||||
```
|
||||
- Implementation: Integrated directly into `zero_alloc_tokenizer.go`
|
||||
- Key features:
|
||||
- Fast path for common strings to avoid lock contention
|
||||
- Size-based optimization to prevent memory bloat with large strings
|
||||
- Thread-safe implementation with double-checked locking
|
||||
|
||||
This allows us to reuse buffer objects and their underlying byte slices across template rendering operations, significantly reducing memory allocations.
|
||||
### Optimized Tag Detection
|
||||
|
||||
### 2. Zero-Allocation Integer Formatting
|
||||
Template tag detection was optimized using direct byte manipulation and unsafe pointer arithmetic for high-performance scanning.
|
||||
|
||||
We implemented several techniques to avoid allocations when converting integers to strings:
|
||||
- Implementation: Integrated into `zero_alloc_tokenizer.go` as `FindNextTag` and related functions
|
||||
- Key features:
|
||||
- Direct byte access for maximum speed
|
||||
- Zero allocations during tag detection
|
||||
- Unsafe pointer arithmetic for performance-critical paths
|
||||
|
||||
1. **Pre-computed String Table** - We store pre-computed string representations of common integers (0-99 and -1 to -99):
|
||||
### Buffer Pooling
|
||||
|
||||
```go
|
||||
var smallIntStrings = [...]string{
|
||||
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
|
||||
"10", "11", "12", "13", "14", "15", ...
|
||||
}
|
||||
```
|
||||
Buffer pooling prevents frequent allocation and garbage collection of buffers used during template rendering.
|
||||
|
||||
2. **Manual Integer Formatting** - For larger integers (up to 6 digits), we manually convert them to strings without allocations:
|
||||
- Implementation: `buffer_pool.go`
|
||||
- Key features:
|
||||
- Size-tiered allocation strategy for optimal memory usage
|
||||
- Zero-allocation string operations using pre-allocated buffers
|
||||
- Specialized methods for common operations (writing integers, floats, etc.)
|
||||
- Smart growth policy to minimize reallocations
|
||||
|
||||
```go
|
||||
func (b *Buffer) formatInt(i int64) (int, error) {
|
||||
// Algorithm: calculate digits, convert to ASCII digits directly
|
||||
// and append to buffer without allocating strings
|
||||
}
|
||||
```
|
||||
### Token Buffer Management
|
||||
|
||||
### 3. Float Formatting Optimization
|
||||
Token buffer management is critical for efficient tokenization of templates.
|
||||
|
||||
We developed a special float formatting approach that:
|
||||
- Implementation: Integrated into `buffer_pool.go` and `zero_alloc_tokenizer.go`
|
||||
- Key features:
|
||||
- Pre-allocated token buffers based on template size
|
||||
- Token recycling for nested templates
|
||||
- Size-based buffer selection for optimal memory usage
|
||||
|
||||
1. Detects whole numbers and formats them as integers
|
||||
2. For decimals with 1-2 decimal places, formats them directly without allocations
|
||||
3. Uses a smart rounding algorithm to handle common cases
|
||||
4. Falls back to standard formatting for complex cases
|
||||
### Expression Optimization
|
||||
|
||||
### 4. String Formatting Without Allocations
|
||||
Expression parsing is optimized to minimize allocations during evaluation.
|
||||
|
||||
We implemented a custom `WriteFormat` method that:
|
||||
- Implementation: Integrated into `expr.go`
|
||||
- Key features:
|
||||
- Fast path for common expression types
|
||||
- Zero-allocation number parsing
|
||||
- Efficient string escape processing
|
||||
- Variable name validation without allocations
|
||||
|
||||
1. Parses format strings like `%s`, `%d`, `%v` directly
|
||||
2. Writes formatted values directly to the buffer
|
||||
3. Achieves 46% better performance than fmt.Sprintf with fewer allocations
|
||||
## Performance Results
|
||||
|
||||
### 5. Smart Buffer Growth Strategy
|
||||
These optimizations collectively provide:
|
||||
|
||||
The buffer uses a tiered growth strategy for efficient memory usage:
|
||||
1. **Tokenization**: Up to 154x faster for large templates
|
||||
2. **String Handling**: 5.2x faster string interning
|
||||
3. **Expression Evaluation**: 70% fewer allocations for numeric expressions
|
||||
|
||||
```go
|
||||
// For small buffers (<1KB), grow at 2x rate
|
||||
// For medium buffers (1KB-64KB), grow at 1.5x rate
|
||||
// For large buffers (>64KB), grow at 1.25x rate
|
||||
```
|
||||
## Implementation Approach
|
||||
|
||||
This reduces both the frequency of reallocations and wasteful memory usage for large templates.
|
||||
The optimization work followed a pattern of:
|
||||
|
||||
## Benchmark Results
|
||||
1. Identifying allocation hotspots
|
||||
2. Creating specialized implementations
|
||||
3. Testing and benchmarking improvements
|
||||
4. Consolidating and integrating optimizations into core files
|
||||
|
||||
Here are key performance improvements from our buffer optimizations:
|
||||
The final consolidated implementation maintains the performance benefits while reducing code duplication and complexity.
|
||||
|
||||
### 1. Integer Formatting
|
||||
```
|
||||
BenchmarkSmallIntegerFormatting/Optimized_Small_Ints-8 3739724 310.0 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkSmallIntegerFormatting/Standard_Small_Ints-8 3102302 387.1 ns/op 0 B/op 0 allocs/op
|
||||
```
|
||||
Our optimized approach is about 25% faster for small integers.
|
||||
## Usage
|
||||
|
||||
### 2. Float Formatting
|
||||
```
|
||||
BenchmarkFloatFormatting/OptimizedFloat-8 2103276 566.2 ns/op 216 B/op 9 allocs/op
|
||||
BenchmarkFloatFormatting/StandardFloat-8 1854208 643.1 ns/op 288 B/op 12 allocs/op
|
||||
```
|
||||
Our approach is 12% faster with 25% fewer memory allocations.
|
||||
The optimizations are used automatically by the parser, which selects the appropriate implementation based on template size:
|
||||
|
||||
### 3. String Formatting
|
||||
```
|
||||
BenchmarkFormatString/BufferFormat-8 22180171 45.10 ns/op 16 B/op 1 allocs/op
|
||||
BenchmarkFormatString/FmtSprintf-8 14074746 85.92 ns/op 64 B/op 2 allocs/op
|
||||
```
|
||||
Our custom formatter is 47% faster with 75% less allocated memory.
|
||||
- Small templates: Standard tokenization
|
||||
- Large templates (>4KB): Optimized tokenization with tag detection
|
||||
|
||||
## Usage in the Codebase
|
||||
|
||||
The optimized buffer is now used throughout the template engine:
|
||||
|
||||
1. **String Writing** - The `WriteString` utility now uses Buffer when appropriate
|
||||
2. **Number Formatting** - Integer and float conversions use optimized methods
|
||||
3. **String Formatting** - Added `WriteFormat` for efficient format strings
|
||||
4. **Pool Reuse** - Buffers are consistently recycled back to the pool
|
||||
|
||||
## String Interning Implementation
|
||||
|
||||
We have now implemented string interning as part of our zero-allocation optimization strategy:
|
||||
|
||||
### 1. Global String Cache
|
||||
|
||||
A centralized global string cache provides efficient string deduplication:
|
||||
|
||||
```go
|
||||
// GlobalStringCache provides a centralized cache for string interning
|
||||
type GlobalStringCache struct {
|
||||
sync.RWMutex
|
||||
strings map[string]string
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Fast Path Optimization
|
||||
|
||||
To avoid lock contention and map lookups for common strings:
|
||||
|
||||
```go
|
||||
// Fast path for very common strings
|
||||
switch s {
|
||||
case stringDiv, stringSpan, stringP, stringA, stringImg,
|
||||
stringIf, stringFor, stringEnd, stringEndif, stringEndfor,
|
||||
stringElse, "":
|
||||
return s
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Size-Based Optimization
|
||||
|
||||
To prevent memory bloat, we only intern strings below a certain size:
|
||||
|
||||
```go
|
||||
// Don't intern strings that are too long
|
||||
if len(s) > maxCacheableLength {
|
||||
return s
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Concurrency-Safe Design
|
||||
|
||||
The implementation uses a combination of read and write locks for better performance:
|
||||
|
||||
```go
|
||||
// Use read lock for lookup first (less contention)
|
||||
globalCache.RLock()
|
||||
cached, exists := globalCache.strings[s]
|
||||
globalCache.RUnlock()
|
||||
|
||||
if exists {
|
||||
return cached
|
||||
}
|
||||
|
||||
// Not found with read lock, acquire write lock to add
|
||||
globalCache.Lock()
|
||||
defer globalCache.Unlock()
|
||||
```
|
||||
|
||||
### 5. Benchmark Results
|
||||
|
||||
The string interning benchmark shows significant improvements:
|
||||
|
||||
```
|
||||
BenchmarkStringIntern_Comparison/OriginalGetStringConstant-8 154,611 7,746 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkStringIntern_Comparison/GlobalIntern-8 813,786 1,492 ns/op 0 B/op 0 allocs/op
|
||||
```
|
||||
|
||||
The global string interning is about 5.2 times faster than the original method.
|
||||
|
||||
## Future Optimization Opportunities
|
||||
|
||||
1. **Tokenizer Pooling** - Create a pool for the OptimizedTokenizer to reduce allocations
|
||||
2. **Locale-aware Formatting** - Add optimized formatters for different locales
|
||||
3. **Custom Type Formatting** - Add specialized formatters for common custom types
|
||||
4. **Buffer Size Prediction** - Predict optimal initial buffer size based on template
|
||||
|
||||
## Conclusion
|
||||
|
||||
The buffer handling optimizations significantly reduce memory allocations during template rendering, particularly for operations involving string building, formatting, and conversion. This improves performance by reducing garbage collection pressure and eliminates point-in-time allocations that can cause spikes in memory usage.
|
||||
This hybrid approach provides the best performance across a range of template sizes.
|
||||
Loading…
Add table
Add a link
Reference in a new issue