From 09ff4873e3804d1a5d8ba6ac7f3cd453f6fd04a8 Mon Sep 17 00:00:00 2001 From: Ville Vesilehto Date: Mon, 19 Jan 2026 22:45:21 +0200 Subject: [PATCH] perf(vm): optimize loop iteration with scope pool Iteration over slices previously used reflection to access elements, which was slow and allocated unnecessarily. This change adds type-specialized fast paths for common slice types ([]int, []float64, []string, []any) that bypass reflection entirely. Scope objects are now pooled and reused across loop iterations. The current scope pointer is cached to avoid repeated slice lookups. Signed-off-by: Ville Vesilehto --- vm/utils.go | 5 +++ vm/vm.go | 125 ++++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 102 insertions(+), 28 deletions(-) diff --git a/vm/utils.go b/vm/utils.go index 11005137c..e880a8a40 100644 --- a/vm/utils.go +++ b/vm/utils.go @@ -20,6 +20,11 @@ type Scope struct { Len int Count int Acc any + // Fast paths + Ints []int + Floats []float64 + Strings []string + Anys []any } type groupBy = map[any][]any diff --git a/vm/vm.go b/vm/vm.go index 69c379b2a..0000d86ff 100644 --- a/vm/vm.go +++ b/vm/vm.go @@ -46,6 +46,9 @@ type VM struct { debug bool step chan struct{} curr chan int + scopePool []Scope // Pre-allocated pool of Scope values for reuse + scopePoolIdx int // Current index into scopePool for allocation + currScope *Scope // Cached pointer to the current scope (optimization) } func (vm *VM) Run(program *Program, env any) (_ any, err error) { @@ -76,6 +79,8 @@ func (vm *VM) Run(program *Program, env any) (_ any, err error) { clearSlice(vm.Scopes) vm.Scopes = vm.Scopes[0:0] } + vm.scopePoolIdx = 0 // Reset pool index for reuse + vm.currScope = nil if len(vm.Variables) < program.variables { vm.Variables = make([]any, program.variables) } @@ -221,8 +226,7 @@ func (vm *VM) Run(program *Program, env any) (_ any, err error) { if arg < 0 { panic("negative jump offset is invalid") } - scope := vm.scope() - if scope.Index >= scope.Len { + if vm.currScope.Index >= vm.currScope.Len { vm.ip += arg } @@ -511,40 +515,45 @@ func (vm *VM) Run(program *Program, env any) (_ any, err error) { vm.push(deref.Interface(a)) case OpIncrementIndex: - vm.scope().Index++ + vm.currScope.Index++ case OpDecrementIndex: - scope := vm.scope() - scope.Index-- + vm.currScope.Index-- case OpIncrementCount: - scope := vm.scope() - scope.Count++ + vm.currScope.Count++ case OpGetIndex: - vm.push(vm.scope().Index) + vm.push(vm.currScope.Index) case OpGetCount: - scope := vm.scope() - vm.push(scope.Count) + vm.push(vm.currScope.Count) case OpGetLen: - scope := vm.scope() - vm.push(scope.Len) + vm.push(vm.currScope.Len) case OpGetAcc: - vm.push(vm.scope().Acc) + vm.push(vm.currScope.Acc) case OpSetAcc: - vm.scope().Acc = vm.pop() + vm.currScope.Acc = vm.pop() case OpSetIndex: - scope := vm.scope() - scope.Index = vm.pop().(int) + vm.currScope.Index = vm.pop().(int) case OpPointer: - scope := vm.scope() - vm.push(scope.Array.Index(scope.Index).Interface()) + scope := vm.currScope + if scope.Ints != nil { + vm.push(scope.Ints[scope.Index]) + } else if scope.Floats != nil { + vm.push(scope.Floats[scope.Index]) + } else if scope.Strings != nil { + vm.push(scope.Strings[scope.Index]) + } else if scope.Anys != nil { + vm.push(scope.Anys[scope.Index]) + } else { + vm.push(scope.Array.Index(scope.Index).Interface()) + } case OpThrow: panic(vm.pop().(error)) @@ -554,7 +563,7 @@ func (vm *VM) Run(program *Program, env any) (_ any, err error) { case 1: vm.push(make(groupBy)) case 2: - scope := vm.scope() + scope := vm.currScope var desc bool switch vm.pop().(string) { case "asc": @@ -574,21 +583,43 @@ func (vm *VM) Run(program *Program, env any) (_ any, err error) { } case OpGroupBy: - scope := vm.scope() + scope := vm.currScope key := vm.pop() - item := scope.Array.Index(scope.Index).Interface() + var item any + if scope.Ints != nil { + item = scope.Ints[scope.Index] + } else if scope.Floats != nil { + item = scope.Floats[scope.Index] + } else if scope.Strings != nil { + item = scope.Strings[scope.Index] + } else if scope.Anys != nil { + item = scope.Anys[scope.Index] + } else { + item = scope.Array.Index(scope.Index).Interface() + } scope.Acc.(groupBy)[key] = append(scope.Acc.(groupBy)[key], item) case OpSortBy: - scope := vm.scope() + scope := vm.currScope value := vm.pop() - item := scope.Array.Index(scope.Index).Interface() + var item any + if scope.Ints != nil { + item = scope.Ints[scope.Index] + } else if scope.Floats != nil { + item = scope.Floats[scope.Index] + } else if scope.Strings != nil { + item = scope.Strings[scope.Index] + } else if scope.Anys != nil { + item = scope.Anys[scope.Index] + } else { + item = scope.Array.Index(scope.Index).Interface() + } sortable := scope.Acc.(*runtime.SortBy) sortable.Array = append(sortable.Array, item) sortable.Values = append(sortable.Values, value) case OpSort: - scope := vm.scope() + scope := vm.currScope sortable := scope.Acc.(*runtime.SortBy) sort.Sort(sortable) vm.memGrow(uint(scope.Len)) @@ -605,10 +636,23 @@ func (vm *VM) Run(program *Program, env any) (_ any, err error) { case OpBegin: a := vm.pop() array := reflect.ValueOf(a) - vm.Scopes = append(vm.Scopes, &Scope{ - Array: array, - Len: array.Len(), - }) + s := vm.allocScope(array) + switch v := a.(type) { + case []int: + s.Ints = v + s.Len = len(v) + case []float64: + s.Floats = v + s.Len = len(v) + case []string: + s.Strings = v + s.Len = len(v) + case []any: + s.Anys = v + s.Len = len(v) + } + vm.Scopes = append(vm.Scopes, s) + vm.currScope = s case OpAnd: a := vm.pop() @@ -622,6 +666,11 @@ func (vm *VM) Run(program *Program, env any) (_ any, err error) { case OpEnd: vm.Scopes = vm.Scopes[:len(vm.Scopes)-1] + if len(vm.Scopes) > 0 { + vm.currScope = vm.Scopes[len(vm.Scopes)-1] + } else { + vm.currScope = nil + } default: panic(fmt.Sprintf("unknown bytecode %#x", op)) @@ -675,6 +724,26 @@ func (vm *VM) scope() *Scope { return vm.Scopes[len(vm.Scopes)-1] } +// allocScope returns a pointer to a Scope from the pool, growing the pool if needed. +// The returned Scope has Array and Len set; Index, Count, and Acc are zeroed. +func (vm *VM) allocScope(array reflect.Value) *Scope { + if vm.scopePoolIdx >= len(vm.scopePool) { + vm.scopePool = append(vm.scopePool, Scope{}) + } + s := &vm.scopePool[vm.scopePoolIdx] + vm.scopePoolIdx++ + s.Array = array + s.Len = array.Len() + s.Index = 0 + s.Count = 0 + s.Acc = nil + s.Ints = nil + s.Floats = nil + s.Strings = nil + s.Anys = nil + return s +} + // getArgsForFunc lazily initializes the buffer the first time it is called for // a given program (thus, it also needs "program" to run). It will // take "needed" elements from the buffer and populate them with vm.pop() in