Skip to content

Commit d557072

Browse files
committed
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 <ville@vesilehto.fi>
1 parent 556b1d1 commit d557072

File tree

2 files changed

+102
-28
lines changed

2 files changed

+102
-28
lines changed

vm/utils.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ type Scope struct {
2020
Len int
2121
Count int
2222
Acc any
23+
// Fast paths
24+
Ints []int
25+
Floats []float64
26+
Strings []string
27+
Anys []any
2328
}
2429

2530
type groupBy = map[any][]any

vm/vm.go

Lines changed: 97 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ type VM struct {
4646
debug bool
4747
step chan struct{}
4848
curr chan int
49+
scopePool []Scope // Pre-allocated pool of Scope values for reuse
50+
scopePoolIdx int // Current index into scopePool for allocation
51+
currScope *Scope // Cached pointer to the current scope (optimization)
4952
}
5053

5154
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) {
7679
clearSlice(vm.Scopes)
7780
vm.Scopes = vm.Scopes[0:0]
7881
}
82+
vm.scopePoolIdx = 0 // Reset pool index for reuse
83+
vm.currScope = nil
7984
if len(vm.Variables) < program.variables {
8085
vm.Variables = make([]any, program.variables)
8186
}
@@ -221,8 +226,7 @@ func (vm *VM) Run(program *Program, env any) (_ any, err error) {
221226
if arg < 0 {
222227
panic("negative jump offset is invalid")
223228
}
224-
scope := vm.scope()
225-
if scope.Index >= scope.Len {
229+
if vm.currScope.Index >= vm.currScope.Len {
226230
vm.ip += arg
227231
}
228232

@@ -511,40 +515,45 @@ func (vm *VM) Run(program *Program, env any) (_ any, err error) {
511515
vm.push(deref.Interface(a))
512516

513517
case OpIncrementIndex:
514-
vm.scope().Index++
518+
vm.currScope.Index++
515519

516520
case OpDecrementIndex:
517-
scope := vm.scope()
518-
scope.Index--
521+
vm.currScope.Index--
519522

520523
case OpIncrementCount:
521-
scope := vm.scope()
522-
scope.Count++
524+
vm.currScope.Count++
523525

524526
case OpGetIndex:
525-
vm.push(vm.scope().Index)
527+
vm.push(vm.currScope.Index)
526528

527529
case OpGetCount:
528-
scope := vm.scope()
529-
vm.push(scope.Count)
530+
vm.push(vm.currScope.Count)
530531

531532
case OpGetLen:
532-
scope := vm.scope()
533-
vm.push(scope.Len)
533+
vm.push(vm.currScope.Len)
534534

535535
case OpGetAcc:
536-
vm.push(vm.scope().Acc)
536+
vm.push(vm.currScope.Acc)
537537

538538
case OpSetAcc:
539-
vm.scope().Acc = vm.pop()
539+
vm.currScope.Acc = vm.pop()
540540

541541
case OpSetIndex:
542-
scope := vm.scope()
543-
scope.Index = vm.pop().(int)
542+
vm.currScope.Index = vm.pop().(int)
544543

545544
case OpPointer:
546-
scope := vm.scope()
547-
vm.push(scope.Array.Index(scope.Index).Interface())
545+
scope := vm.currScope
546+
if scope.Ints != nil {
547+
vm.push(scope.Ints[scope.Index])
548+
} else if scope.Floats != nil {
549+
vm.push(scope.Floats[scope.Index])
550+
} else if scope.Strings != nil {
551+
vm.push(scope.Strings[scope.Index])
552+
} else if scope.Anys != nil {
553+
vm.push(scope.Anys[scope.Index])
554+
} else {
555+
vm.push(scope.Array.Index(scope.Index).Interface())
556+
}
548557

549558
case OpThrow:
550559
panic(vm.pop().(error))
@@ -554,7 +563,7 @@ func (vm *VM) Run(program *Program, env any) (_ any, err error) {
554563
case 1:
555564
vm.push(make(groupBy))
556565
case 2:
557-
scope := vm.scope()
566+
scope := vm.currScope
558567
var desc bool
559568
switch vm.pop().(string) {
560569
case "asc":
@@ -574,21 +583,43 @@ func (vm *VM) Run(program *Program, env any) (_ any, err error) {
574583
}
575584

576585
case OpGroupBy:
577-
scope := vm.scope()
586+
scope := vm.currScope
578587
key := vm.pop()
579-
item := scope.Array.Index(scope.Index).Interface()
588+
var item any
589+
if scope.Ints != nil {
590+
item = scope.Ints[scope.Index]
591+
} else if scope.Floats != nil {
592+
item = scope.Floats[scope.Index]
593+
} else if scope.Strings != nil {
594+
item = scope.Strings[scope.Index]
595+
} else if scope.Anys != nil {
596+
item = scope.Anys[scope.Index]
597+
} else {
598+
item = scope.Array.Index(scope.Index).Interface()
599+
}
580600
scope.Acc.(groupBy)[key] = append(scope.Acc.(groupBy)[key], item)
581601

582602
case OpSortBy:
583-
scope := vm.scope()
603+
scope := vm.currScope
584604
value := vm.pop()
585-
item := scope.Array.Index(scope.Index).Interface()
605+
var item any
606+
if scope.Ints != nil {
607+
item = scope.Ints[scope.Index]
608+
} else if scope.Floats != nil {
609+
item = scope.Floats[scope.Index]
610+
} else if scope.Strings != nil {
611+
item = scope.Strings[scope.Index]
612+
} else if scope.Anys != nil {
613+
item = scope.Anys[scope.Index]
614+
} else {
615+
item = scope.Array.Index(scope.Index).Interface()
616+
}
586617
sortable := scope.Acc.(*runtime.SortBy)
587618
sortable.Array = append(sortable.Array, item)
588619
sortable.Values = append(sortable.Values, value)
589620

590621
case OpSort:
591-
scope := vm.scope()
622+
scope := vm.currScope
592623
sortable := scope.Acc.(*runtime.SortBy)
593624
sort.Sort(sortable)
594625
vm.memGrow(uint(scope.Len))
@@ -605,10 +636,23 @@ func (vm *VM) Run(program *Program, env any) (_ any, err error) {
605636
case OpBegin:
606637
a := vm.pop()
607638
array := reflect.ValueOf(a)
608-
vm.Scopes = append(vm.Scopes, &Scope{
609-
Array: array,
610-
Len: array.Len(),
611-
})
639+
s := vm.allocScope(array)
640+
switch v := a.(type) {
641+
case []int:
642+
s.Ints = v
643+
s.Len = len(v)
644+
case []float64:
645+
s.Floats = v
646+
s.Len = len(v)
647+
case []string:
648+
s.Strings = v
649+
s.Len = len(v)
650+
case []any:
651+
s.Anys = v
652+
s.Len = len(v)
653+
}
654+
vm.Scopes = append(vm.Scopes, s)
655+
vm.currScope = s
612656

613657
case OpAnd:
614658
a := vm.pop()
@@ -622,6 +666,11 @@ func (vm *VM) Run(program *Program, env any) (_ any, err error) {
622666

623667
case OpEnd:
624668
vm.Scopes = vm.Scopes[:len(vm.Scopes)-1]
669+
if len(vm.Scopes) > 0 {
670+
vm.currScope = vm.Scopes[len(vm.Scopes)-1]
671+
} else {
672+
vm.currScope = nil
673+
}
625674

626675
default:
627676
panic(fmt.Sprintf("unknown bytecode %#x", op))
@@ -675,6 +724,26 @@ func (vm *VM) scope() *Scope {
675724
return vm.Scopes[len(vm.Scopes)-1]
676725
}
677726

727+
// allocScope returns a pointer to a Scope from the pool, growing the pool if needed.
728+
// The returned Scope has Array and Len set; Index, Count, and Acc are zeroed.
729+
func (vm *VM) allocScope(array reflect.Value) *Scope {
730+
if vm.scopePoolIdx >= len(vm.scopePool) {
731+
vm.scopePool = append(vm.scopePool, Scope{})
732+
}
733+
s := &vm.scopePool[vm.scopePoolIdx]
734+
vm.scopePoolIdx++
735+
s.Array = array
736+
s.Len = array.Len()
737+
s.Index = 0
738+
s.Count = 0
739+
s.Acc = nil
740+
s.Ints = nil
741+
s.Floats = nil
742+
s.Strings = nil
743+
s.Anys = nil
744+
return s
745+
}
746+
678747
// getArgsForFunc lazily initializes the buffer the first time it is called for
679748
// a given program (thus, it also needs "program" to run). It will
680749
// take "needed" elements from the buffer and populate them with vm.pop() in

0 commit comments

Comments
 (0)