@@ -17,6 +17,8 @@ import (
1717 "github.com/expr-lang/expr/vm/runtime"
1818)
1919
20+ const maxFnArgsBuf = 256
21+
2022func Run (program * Program , env any ) (any , error ) {
2123 if program == nil {
2224 return nil , fmt .Errorf ("program is nil" )
@@ -83,6 +85,8 @@ func (vm *VM) Run(program *Program, env any) (_ any, err error) {
8385 vm .memory = 0
8486 vm .ip = 0
8587
88+ var fnArgsBuf []any
89+
8690 for vm .ip < len (program .Bytecode ) {
8791 if debug && vm .debug {
8892 <- vm .step
@@ -399,62 +403,53 @@ func (vm *VM) Run(program *Program, env any) (_ any, err error) {
399403 vm .push (out )
400404
401405 case OpCall1 :
402- a := vm .pop ()
403- out , err := program.functions [arg ](a )
406+ var args []any
407+ args , fnArgsBuf = vm .getArgsForFunc (fnArgsBuf , program , 1 )
408+ out , err := program .functions [arg ](args ... )
404409 if err != nil {
405410 panic (err )
406411 }
407412 vm .push (out )
408413
409414 case OpCall2 :
410- b := vm . pop ()
411- a : = vm .pop ( )
412- out , err := program .functions [arg ](a , b )
415+ var args [] any
416+ args , fnArgsBuf = vm .getArgsForFunc ( fnArgsBuf , program , 2 )
417+ out , err := program .functions [arg ](args ... )
413418 if err != nil {
414419 panic (err )
415420 }
416421 vm .push (out )
417422
418423 case OpCall3 :
419- c := vm .pop ()
420- b := vm .pop ()
421- a := vm .pop ()
422- out , err := program .functions [arg ](a , b , c )
424+ var args []any
425+ args , fnArgsBuf = vm .getArgsForFunc (fnArgsBuf , program , 3 )
426+ out , err := program .functions [arg ](args ... )
423427 if err != nil {
424428 panic (err )
425429 }
426430 vm .push (out )
427431
428432 case OpCallN :
429433 fn := vm .pop ().(Function )
430- size := arg
431- in := make ([]any , size )
432- for i := int (size ) - 1 ; i >= 0 ; i -- {
433- in [i ] = vm .pop ()
434- }
435- out , err := fn (in ... )
434+ var args []any
435+ args , fnArgsBuf = vm .getArgsForFunc (fnArgsBuf , program , arg )
436+ out , err := fn (args ... )
436437 if err != nil {
437438 panic (err )
438439 }
439440 vm .push (out )
440441
441442 case OpCallFast :
442443 fn := vm .pop ().(func (... any ) any )
443- size := arg
444- in := make ([]any , size )
445- for i := int (size ) - 1 ; i >= 0 ; i -- {
446- in [i ] = vm .pop ()
447- }
448- vm .push (fn (in ... ))
444+ var args []any
445+ args , fnArgsBuf = vm .getArgsForFunc (fnArgsBuf , program , arg )
446+ vm .push (fn (args ... ))
449447
450448 case OpCallSafe :
451449 fn := vm .pop ().(SafeFunction )
452- size := arg
453- in := make ([]any , size )
454- for i := int (size ) - 1 ; i >= 0 ; i -- {
455- in [i ] = vm .pop ()
456- }
457- out , mem , err := fn (in ... )
450+ var args []any
451+ args , fnArgsBuf = vm .getArgsForFunc (fnArgsBuf , program , arg )
452+ out , mem , err := fn (args ... )
458453 if err != nil {
459454 panic (err )
460455 }
@@ -671,6 +666,64 @@ func (vm *VM) scope() *Scope {
671666 return vm .Scopes [len (vm .Scopes )- 1 ]
672667}
673668
669+ // getArgsForFunc lazily initializes the buffer the first time it is called for
670+ // a given program (thus, it also needs "program" to run). It will
671+ // take "needed" elements from the buffer and populate them with vm.pop() in
672+ // reverse order. Because the estimation can fall short, this function can
673+ // occasionally make a new allocation.
674+ func (vm * VM ) getArgsForFunc (argsBuf []any , program * Program , needed int ) (args []any , argsBufOut []any ) {
675+ if needed == 0 || program == nil {
676+ return nil , argsBuf
677+ }
678+
679+ // Step 1: fix estimations and preallocate
680+ if argsBuf == nil {
681+ estimatedFnArgsCount := estimateFnArgsCount (program )
682+ if estimatedFnArgsCount > maxFnArgsBuf {
683+ // put a practical limit to avoid excessive preallocation
684+ estimatedFnArgsCount = maxFnArgsBuf
685+ }
686+ if estimatedFnArgsCount < needed {
687+ // in the case that the first call is for example OpCallN with a large
688+ // number of arguments, then make sure we will be able to serve them at
689+ // least.
690+ estimatedFnArgsCount = needed
691+ }
692+
693+ // in the case that we are preparing the arguments for the first
694+ // function call of the program, then argsBuf will be nil, so we
695+ // initialize it. We delay this initial allocation here because a
696+ // program could have many function calls but exit earlier than the
697+ // first call, so in that case we avoid allocating unnecessarily
698+ argsBuf = make ([]any , estimatedFnArgsCount )
699+ }
700+
701+ // Step 2: get the final slice that will be returned
702+ var buf []any
703+ if len (argsBuf ) >= needed {
704+ // in this case, we are successfully using the single preallocation. We
705+ // use the full slice expression [low : high : max] because in that way
706+ // a function that receives this slice as variadic arguments will not be
707+ // able to make modifications to contiguous elements with append(). If
708+ // they call append on their variadic arguments they will make a new
709+ // allocation.
710+ buf = (argsBuf )[:needed :needed ]
711+ argsBuf = (argsBuf )[needed :] // advance the buffer
712+ } else {
713+ // if we have been making calls to something like OpCallN with many more
714+ // arguments than what we estimated, then we will need to allocate
715+ // separately
716+ buf = make ([]any , needed )
717+ }
718+
719+ // Step 3: populate the final slice bulk copying from the stack. This is the
720+ // exact order and copy() is a highly optimized operation
721+ copy (buf , vm .Stack [len (vm .Stack )- needed :])
722+ vm .Stack = vm .Stack [:len (vm .Stack )- needed ]
723+
724+ return buf , argsBuf
725+ }
726+
674727func (vm * VM ) Step () {
675728 vm .step <- struct {}{}
676729}
@@ -685,3 +738,30 @@ func clearSlice[S ~[]E, E any](s S) {
685738 s [i ] = zero // clear mem, optimized by the compiler, in Go 1.21 the "clear" builtin can be used
686739 }
687740}
741+
742+ // estimateFnArgsCount inspects a *Program and estimates how many function
743+ // arguments will be required to run it.
744+ func estimateFnArgsCount (program * Program ) int {
745+ // Implementation note: a program will not necessarily go through all
746+ // operations, but this is just an estimation
747+ var count int
748+ for _ , op := range program .Bytecode {
749+ if int (op ) < len (opArgLenEstimation ) {
750+ count += opArgLenEstimation [op ]
751+ }
752+ }
753+ return count
754+ }
755+
756+ var opArgLenEstimation = [... ]int {
757+ OpCall1 : 1 ,
758+ OpCall2 : 2 ,
759+ OpCall3 : 3 ,
760+ // we don't know exactly but we know at least 4, so be conservative as this
761+ // is only an optimization and we also want to avoid excessive preallocation
762+ OpCallN : 4 ,
763+ // here we don't know either, but we can guess it could be common to receive
764+ // up to 3 arguments in a function
765+ OpCallFast : 3 ,
766+ OpCallSafe : 3 ,
767+ }
0 commit comments