Skip to content

Commit 2c1e339

Browse files
authored
refactor(eval): avoid string splitting allocation in GenericParameter (#547)
- Remove redundant pathSegments cache from GenericParameter (cache layer already prevents repeated get() calls) - Replace strings.Split with character-by-character iteration in get() - Optimize prefixPatternParameter.Get() using IndexByte instead of Split/Join
1 parent 6804ae7 commit 2c1e339

File tree

1 file changed

+73
-67
lines changed

1 file changed

+73
-67
lines changed

eval/parameter.go

Lines changed: 73 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -195,11 +195,6 @@ type GenericParameter struct {
195195
// it will cache the final street value to avoid parsing the path again.
196196
cache map[string]reflect.Value
197197

198-
// pathSegments caches the split path segments to avoid repeated strings.Split operations.
199-
// For example, "user.address.street" will be cached as ["user", "address", "street"].
200-
// This optimization reduces CPU overhead when the same parameter paths are accessed repeatedly.
201-
pathSegments map[string][]string
202-
203198
// structFieldIndex caches the field indexes for struct types at each path level.
204199
// The first key is the position in the path (e.g., for "user.address.street": 0 for user, 1 for address).
205200
// The second key is the concrete type of the struct, which ensures correct field lookup for different struct types.
@@ -215,67 +210,68 @@ func (g *GenericParameter) get(name string) (reflect.Value, bool) {
215210
value = g.Value
216211
)
217212

218-
// get or split the path segments
219-
// to avoid repeated strings.Split operations
220-
// cache the split segments
221-
segments, exists := g.pathSegments[name]
222-
if !exists {
223-
segments = strings.Split(name, ".")
224-
if g.pathSegments == nil {
225-
g.pathSegments = make(map[string][]string)
226-
}
227-
g.pathSegments[name] = segments
228-
}
229-
230-
for i, item := range segments {
231-
232-
// only unwrap when the value need to call Get method
233-
value = reflectlite.Unwrap(value)
234-
235-
// match the value type
236-
// only map, struct, slice and array can be wrapped as parameter
237-
switch value.Kind() {
238-
case reflect.Map:
239-
// if the map key is not a string type, then return false
240-
if value.Type().Key().Kind() != reflect.String {
241-
return reflect.Value{}, false
242-
}
243-
param = mapParameter{Value: value}
244-
case reflect.Struct:
245-
// Initialize the three-level cache if not exists:
246-
// Level 1: path position -> to handle different levels in the path (e.g., user.address.street)
247-
// Level 2: concrete type -> to handle different struct types at the same position
248-
// Level 3: field name -> to cache the actual field indexes
249-
if g.structFieldIndex == nil {
250-
g.structFieldIndex = make(map[int]map[reflect.Type]map[string][]int)
251-
}
252-
253-
// Cache the type to avoid multiple calls to Type()
254-
valueType := value.Type()
213+
// Iterate through the name character by character to avoid strings.Split allocation
214+
start := 0
215+
i := 0 // path segment index for structFieldIndex cache
216+
for j := 0; j <= len(name); j++ {
217+
if j == len(name) || name[j] == '.' {
218+
// Extract the segment between start and j
219+
if j > start { // avoid empty segments
220+
item := name[start:j]
221+
222+
// only unwrap when the value need to call Get method
223+
value = reflectlite.Unwrap(value)
224+
225+
// match the value type
226+
// only map, struct, slice and array can be wrapped as parameter
227+
switch value.Kind() {
228+
case reflect.Map:
229+
// if the map key is not a string type, then return false
230+
if value.Type().Key().Kind() != reflect.String {
231+
return reflect.Value{}, false
232+
}
233+
param = mapParameter{Value: value}
234+
case reflect.Struct:
235+
// Initialize the three-level cache if not exists:
236+
// Level 1: path position -> to handle different levels in the path (e.g., user.address.street)
237+
// Level 2: concrete type -> to handle different struct types at the same position
238+
// Level 3: field name -> to cache the actual field indexes
239+
if g.structFieldIndex == nil {
240+
g.structFieldIndex = make(map[int]map[reflect.Type]map[string][]int)
241+
}
242+
243+
// Cache the type to avoid multiple calls to Type()
244+
valueType := value.Type()
245+
246+
// Get or create the type-level cache for current path position
247+
structFieldIndex, in := g.structFieldIndex[i]
248+
if !in {
249+
// Initialize with the current type to avoid another map lookup
250+
structFieldIndex = map[reflect.Type]map[string][]int{
251+
valueType: {},
252+
}
253+
g.structFieldIndex[i] = structFieldIndex
254+
}
255+
256+
// Create a new structParameter with its field cache pointing to
257+
// the cached indexes for its specific type, ensuring different
258+
// struct types don't share the same field index cache
259+
param = &structParameter{Value: value, fieldIndexes: structFieldIndex[valueType]}
260+
case reflect.Slice, reflect.Array:
261+
param = sliceParameter{Value: value}
262+
default:
263+
// otherwise, return false
264+
return reflect.Value{}, false
265+
}
255266

256-
// Get or create the type-level cache for current path position
257-
structFieldIndex, in := g.structFieldIndex[i]
258-
if !in {
259-
// Initialize with the current type to avoid another map lookup
260-
structFieldIndex = map[reflect.Type]map[string][]int{
261-
valueType: {},
267+
var exists bool
268+
value, exists = param.Get(item)
269+
if !exists {
270+
return reflect.Value{}, false
262271
}
263-
g.structFieldIndex[i] = structFieldIndex
272+
i++
264273
}
265-
266-
// Create a new structParameter with its field cache pointing to
267-
// the cached indexes for its specific type, ensuring different
268-
// struct types don't share the same field index cache
269-
param = &structParameter{Value: value, fieldIndexes: structFieldIndex[valueType]}
270-
case reflect.Slice, reflect.Array:
271-
param = sliceParameter{Value: value}
272-
default:
273-
// otherwise, return false
274-
return reflect.Value{}, false
275-
}
276-
value, exists = param.Get(item)
277-
if !exists {
278-
return reflect.Value{}, false
274+
start = j + 1
279275
}
280276
}
281277
return value, true
@@ -351,21 +347,31 @@ type prefixPatternParameter struct {
351347
}
352348

353349
func (p prefixPatternParameter) Get(name string) (value reflect.Value, exists bool) {
354-
items := strings.Split(name, ".")
355-
prefix := items[0]
350+
// Find the first dot to extract the prefix
351+
dotIdx := strings.IndexByte(name, '.')
352+
353+
var prefix string
354+
if dotIdx == -1 {
355+
// No dot found, the entire name is the prefix
356+
prefix = name
357+
} else {
358+
prefix = name[:dotIdx]
359+
}
356360

357361
if p.prefix != prefix {
358362
return reflect.Value{}, false
359363
}
360364

361-
if len(items) == 1 {
365+
if dotIdx == -1 {
366+
// Only prefix, return the param itself
362367
return reflect.ValueOf(p.param), true
363368
}
364369

365370
if p.parameter == nil {
366371
p.parameter = NewGenericParam(p.param, "")
367372
}
368-
return p.parameter.Get(strings.Join(items[1:], "."))
373+
// Pass the remaining part after the prefix (skip the dot)
374+
return p.parameter.Get(name[dotIdx+1:])
369375
}
370376

371377
// PrefixPatternParameter is a parameter that supports prefix pattern.

0 commit comments

Comments
 (0)