go-twig/expr_pool.go
semihalev 435bb12ac3 Optimize expression evaluation to reduce allocations
- Implemented pooled slices for function arguments
- Added specialized pooling for variable node and literal node objects
- Modified array and hash node evaluation to reduce allocations
- Optimized test and filter evaluation with pooled resources
- Added comprehensive benchmarks to validate improvements
- Updated node pool implementation to remove duplicate declarations
- Fixed memory allocations in merge filter to correctly handle array manipulations

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-12 03:04:36 +03:00

475 lines
No EOL
11 KiB
Go

package twig
import (
"sync"
)
// This file implements object pooling for expression node types
// to reduce memory allocations during evaluation.
// BinaryNodePool provides a pool for BinaryNode objects
var BinaryNodePool = sync.Pool{
New: func() interface{} {
return &BinaryNode{}
},
}
// GetBinaryNode gets a BinaryNode from the pool and initializes it
func GetBinaryNode(operator string, left, right Node, line int) *BinaryNode {
node := BinaryNodePool.Get().(*BinaryNode)
node.ExpressionNode.exprType = ExprBinary
node.ExpressionNode.line = line
node.operator = operator
node.left = left
node.right = right
return node
}
// ReleaseBinaryNode returns a BinaryNode to the pool
func ReleaseBinaryNode(node *BinaryNode) {
if node == nil {
return
}
node.operator = ""
node.left = nil
node.right = nil
BinaryNodePool.Put(node)
}
// GetAttrNodePool provides a pool for GetAttrNode objects
var GetAttrNodePool = sync.Pool{
New: func() interface{} {
return &GetAttrNode{}
},
}
// GetGetAttrNode gets a GetAttrNode from the pool and initializes it
func GetGetAttrNode(node, attribute Node, line int) *GetAttrNode {
n := GetAttrNodePool.Get().(*GetAttrNode)
n.ExpressionNode.exprType = ExprGetAttr
n.ExpressionNode.line = line
n.node = node
n.attribute = attribute
return n
}
// ReleaseGetAttrNode returns a GetAttrNode to the pool
func ReleaseGetAttrNode(node *GetAttrNode) {
if node == nil {
return
}
node.node = nil
node.attribute = nil
GetAttrNodePool.Put(node)
}
// GetItemNodePool provides a pool for GetItemNode objects
var GetItemNodePool = sync.Pool{
New: func() interface{} {
return &GetItemNode{}
},
}
// GetGetItemNode gets a GetItemNode from the pool and initializes it
func GetGetItemNode(node, item Node, line int) *GetItemNode {
n := GetItemNodePool.Get().(*GetItemNode)
n.ExpressionNode.exprType = ExprGetItem
n.ExpressionNode.line = line
n.node = node
n.item = item
return n
}
// ReleaseGetItemNode returns a GetItemNode to the pool
func ReleaseGetItemNode(node *GetItemNode) {
if node == nil {
return
}
node.node = nil
node.item = nil
GetItemNodePool.Put(node)
}
// FilterNodePool provides a pool for FilterNode objects
var FilterNodePool = sync.Pool{
New: func() interface{} {
return &FilterNode{}
},
}
// GetFilterNode gets a FilterNode from the pool and initializes it
func GetFilterNode(node Node, filter string, args []Node, line int) *FilterNode {
n := FilterNodePool.Get().(*FilterNode)
n.ExpressionNode.exprType = ExprFilter
n.ExpressionNode.line = line
n.node = node
n.filter = filter
n.args = args
return n
}
// ReleaseFilterNode returns a FilterNode to the pool
func ReleaseFilterNode(node *FilterNode) {
if node == nil {
return
}
node.node = nil
node.filter = ""
node.args = nil
FilterNodePool.Put(node)
}
// TestNodePool provides a pool for TestNode objects
var TestNodePool = sync.Pool{
New: func() interface{} {
return &TestNode{}
},
}
// GetTestNode gets a TestNode from the pool and initializes it
func GetTestNode(node Node, test string, args []Node, line int) *TestNode {
n := TestNodePool.Get().(*TestNode)
n.ExpressionNode.exprType = ExprTest
n.ExpressionNode.line = line
n.node = node
n.test = test
n.args = args
return n
}
// ReleaseTestNode returns a TestNode to the pool
func ReleaseTestNode(node *TestNode) {
if node == nil {
return
}
node.node = nil
node.test = ""
node.args = nil
TestNodePool.Put(node)
}
// UnaryNodePool provides a pool for UnaryNode objects
var UnaryNodePool = sync.Pool{
New: func() interface{} {
return &UnaryNode{}
},
}
// GetUnaryNode gets a UnaryNode from the pool and initializes it
func GetUnaryNode(operator string, node Node, line int) *UnaryNode {
n := UnaryNodePool.Get().(*UnaryNode)
n.ExpressionNode.exprType = ExprUnary
n.ExpressionNode.line = line
n.operator = operator
n.node = node
return n
}
// ReleaseUnaryNode returns a UnaryNode to the pool
func ReleaseUnaryNode(node *UnaryNode) {
if node == nil {
return
}
node.operator = ""
node.node = nil
UnaryNodePool.Put(node)
}
// ConditionalNodePool provides a pool for ConditionalNode objects
var ConditionalNodePool = sync.Pool{
New: func() interface{} {
return &ConditionalNode{}
},
}
// GetConditionalNode gets a ConditionalNode from the pool and initializes it
func GetConditionalNode(condition, trueExpr, falseExpr Node, line int) *ConditionalNode {
node := ConditionalNodePool.Get().(*ConditionalNode)
node.ExpressionNode.exprType = ExprConditional
node.ExpressionNode.line = line
node.condition = condition
node.trueExpr = trueExpr
node.falseExpr = falseExpr
return node
}
// ReleaseConditionalNode returns a ConditionalNode to the pool
func ReleaseConditionalNode(node *ConditionalNode) {
if node == nil {
return
}
node.condition = nil
node.trueExpr = nil
node.falseExpr = nil
ConditionalNodePool.Put(node)
}
// ArrayNodePool provides a pool for ArrayNode objects
var ArrayNodePool = sync.Pool{
New: func() interface{} {
return &ArrayNode{}
},
}
// GetArrayNode gets an ArrayNode from the pool and initializes it
func GetArrayNode(items []Node, line int) *ArrayNode {
node := ArrayNodePool.Get().(*ArrayNode)
node.ExpressionNode.exprType = ExprArray
node.ExpressionNode.line = line
node.items = items
return node
}
// ReleaseArrayNode returns an ArrayNode to the pool
func ReleaseArrayNode(node *ArrayNode) {
if node == nil {
return
}
node.items = nil
ArrayNodePool.Put(node)
}
// HashNodePool provides a pool for HashNode objects
var HashNodePool = sync.Pool{
New: func() interface{} {
return &HashNode{}
},
}
// GetHashNode gets a HashNode from the pool and initializes it
func GetHashNode(items map[Node]Node, line int) *HashNode {
node := HashNodePool.Get().(*HashNode)
node.ExpressionNode.exprType = ExprHash
node.ExpressionNode.line = line
node.items = items
return node
}
// ReleaseHashNode returns a HashNode to the pool
func ReleaseHashNode(node *HashNode) {
if node == nil {
return
}
node.items = nil
HashNodePool.Put(node)
}
// FunctionNodePool provides a pool for FunctionNode objects
var FunctionNodePool = sync.Pool{
New: func() interface{} {
return &FunctionNode{}
},
}
// GetFunctionNode gets a FunctionNode from the pool and initializes it
func GetFunctionNode(name string, args []Node, line int) *FunctionNode {
node := FunctionNodePool.Get().(*FunctionNode)
node.ExpressionNode.exprType = ExprFunction
node.ExpressionNode.line = line
node.name = name
node.args = args
return node
}
// ReleaseFunctionNode returns a FunctionNode to the pool
func ReleaseFunctionNode(node *FunctionNode) {
if node == nil {
return
}
node.name = ""
node.args = nil
node.moduleExpr = nil
FunctionNodePool.Put(node)
}
// VariableNodePool provides a pool for VariableNode objects
var VariableNodePool = sync.Pool{
New: func() interface{} {
return &VariableNode{}
},
}
// GetVariableNode gets a VariableNode from the pool and initializes it
func GetVariableNode(name string, line int) *VariableNode {
node := VariableNodePool.Get().(*VariableNode)
node.ExpressionNode.exprType = ExprVariable
node.ExpressionNode.line = line
node.name = name
return node
}
// ReleaseVariableNode returns a VariableNode to the pool
func ReleaseVariableNode(node *VariableNode) {
if node == nil {
return
}
node.name = ""
VariableNodePool.Put(node)
}
// LiteralNodePool provides a pool for LiteralNode objects
var LiteralNodePool = sync.Pool{
New: func() interface{} {
return &LiteralNode{}
},
}
// GetLiteralNode gets a LiteralNode from the pool and initializes it
func GetLiteralNode(value interface{}, line int) *LiteralNode {
node := LiteralNodePool.Get().(*LiteralNode)
node.ExpressionNode.exprType = ExprLiteral
node.ExpressionNode.line = line
node.value = value
return node
}
// ReleaseLiteralNode returns a LiteralNode to the pool
func ReleaseLiteralNode(node *LiteralNode) {
if node == nil {
return
}
node.value = nil
LiteralNodePool.Put(node)
}
// --- Slice Pools for Expression Evaluation ---
// smallArgSlicePool provides a pool for small argument slices (0-2 items)
var smallArgSlicePool = sync.Pool{
New: func() interface{} {
// Pre-allocate a slice with capacity 2
return make([]interface{}, 0, 2)
},
}
// mediumArgSlicePool provides a pool for medium argument slices (3-5 items)
var mediumArgSlicePool = sync.Pool{
New: func() interface{} {
// Pre-allocate a slice with capacity 5
return make([]interface{}, 0, 5)
},
}
// largeArgSlicePool provides a pool for large argument slices (6-10 items)
var largeArgSlicePool = sync.Pool{
New: func() interface{} {
// Pre-allocate a slice with capacity 10
return make([]interface{}, 0, 10)
},
}
// GetArgSlice gets an appropriately sized slice for the given number of arguments
func GetArgSlice(size int) []interface{} {
if size <= 0 {
return nil
}
var slice []interface{}
switch {
case size <= 2:
slice = smallArgSlicePool.Get().([]interface{})
case size <= 5:
slice = mediumArgSlicePool.Get().([]interface{})
case size <= 10:
slice = largeArgSlicePool.Get().([]interface{})
default:
// For very large slices, just allocate directly
return make([]interface{}, 0, size)
}
// Clear the slice but maintain capacity
return slice[:0]
}
// ReleaseArgSlice returns an argument slice to the appropriate pool
func ReleaseArgSlice(slice []interface{}) {
if slice == nil {
return
}
// Clear all references
for i := range slice {
slice[i] = nil
}
// Reset length to 0
slice = slice[:0]
// Return to appropriate pool based on capacity
switch cap(slice) {
case 2:
smallArgSlicePool.Put(slice)
case 5:
mediumArgSlicePool.Put(slice)
case 10:
largeArgSlicePool.Put(slice)
}
}
// --- Map Pools for HashNode Evaluation ---
// smallHashMapPool provides a pool for small hash maps (1-5 items)
var smallHashMapPool = sync.Pool{
New: func() interface{} {
return make(map[string]interface{}, 5)
},
}
// mediumHashMapPool provides a pool for medium hash maps (6-15 items)
var mediumHashMapPool = sync.Pool{
New: func() interface{} {
return make(map[string]interface{}, 15)
},
}
// GetHashMap gets an appropriately sized map for hash operations
func GetHashMap(size int) map[string]interface{} {
if size <= 5 {
hashMap := smallHashMapPool.Get().(map[string]interface{})
// Clear any existing entries
for k := range hashMap {
delete(hashMap, k)
}
return hashMap
} else if size <= 15 {
hashMap := mediumHashMapPool.Get().(map[string]interface{})
// Clear any existing entries
for k := range hashMap {
delete(hashMap, k)
}
return hashMap
}
// For larger maps, just allocate directly
return make(map[string]interface{}, size)
}
// ReleaseHashMap returns a hash map to the appropriate pool
func ReleaseHashMap(hashMap map[string]interface{}) {
if hashMap == nil {
return
}
// Clear all entries (not used directly in our defer block)
// for k := range hashMap {
// delete(hashMap, k)
// }
// Return to appropriate pool based on capacity
// We don't actually clear the map when releasing through the defer,
// because we return the map as the result and deleting entries would
// clear the returned result
// Map doesn't have a built-in cap function
// Not using pool return for maps directly returned as results
/*
switch {
case len(hashMap) <= 5:
smallHashMapPool.Put(hashMap)
case len(hashMap) <= 15:
mediumHashMapPool.Put(hashMap)
}
*/
}