From 2387d4dc31edf1d11e5b49f34304eb9206ee2f60 Mon Sep 17 00:00:00 2001 From: Bob Nystrom Date: Wed, 1 Jul 2015 00:00:25 -0700 Subject: [PATCH] Grow the call frame array dynamically. Previously, fibers had a hard-coded limit to how big their stack size is. This limit exists in two forms: the number of distinct call frames (basically the maximum call depth), and the number of unique stack slots. This fixes the first half of this by dynamically allocating the call frame array and growing it as needed. This makes new fibers smallers since they can start with a very small array. Checking and growing as needed doesn't noticeably regress the perf on the other benchmarks, and it makes a new fiber benchmark about 45% faster. The stack array is still hardcoded, but that will be in another commit. --- script/benchmark.py | 2 ++ src/vm/wren_value.c | 32 ++++++++++++++++---------------- src/vm/wren_value.h | 35 +++++++++++++++++++++++++++++++---- src/vm/wren_vm.c | 33 +++++++++++++++++---------------- test/benchmark/fibers.wren | 16 ++++++++++++++++ 5 files changed, 82 insertions(+), 36 deletions(-) create mode 100644 test/benchmark/fibers.wren diff --git a/script/benchmark.py b/script/benchmark.py index 0239c5c5..30f704c5 100755 --- a/script/benchmark.py +++ b/script/benchmark.py @@ -67,6 +67,8 @@ BENCHMARK("fib", r"""317811 317811 317811""") +BENCHMARK("fibers", r"""4999950000""") + BENCHMARK("for", r"""499999500000""") BENCHMARK("method_call", r"""true diff --git a/src/vm/wren_value.c b/src/vm/wren_value.c index 7b6fcfbc..0ee93d36 100644 --- a/src/vm/wren_value.c +++ b/src/vm/wren_value.c @@ -26,6 +26,11 @@ // lookup faster. #define MAP_LOAD_PERCENT 75 +// The number of call frames initially allocated when a fiber is created. Making +// this smaller makes fibers use less memory (at first) but spends more time +// reallocating when the call stack grows. +#define INITIAL_CALL_FRAMES 4 + DEFINE_BUFFER(Value, Value); DEFINE_BUFFER(Method, Method); @@ -134,36 +139,31 @@ ObjClosure* wrenNewClosure(WrenVM* vm, ObjFn* fn) ObjFiber* wrenNewFiber(WrenVM* vm, Obj* fn) { + // Allocate the call frames before the fiber in case it triggers a GC. + CallFrame* frames = ALLOCATE_ARRAY(vm, CallFrame, INITIAL_CALL_FRAMES); + ObjFiber* fiber = ALLOCATE(vm, ObjFiber); initObj(vm, &fiber->obj, OBJ_FIBER, vm->fiberClass); fiber->id = vm->nextFiberId++; - - wrenResetFiber(fiber, fn); - + fiber->frames = frames; + fiber->frameCapacity = INITIAL_CALL_FRAMES; + wrenResetFiber(vm, fiber, fn); + return fiber; } -void wrenResetFiber(ObjFiber* fiber, Obj* fn) +void wrenResetFiber(WrenVM* vm, ObjFiber* fiber, Obj* fn) { // Push the stack frame for the function. fiber->stackTop = fiber->stack; - fiber->numFrames = 1; fiber->openUpvalues = NULL; fiber->caller = NULL; fiber->error = NULL; fiber->callerIsTrying = false; - CallFrame* frame = &fiber->frames[0]; - frame->fn = fn; - frame->stackStart = fiber->stack; - if (fn->type == OBJ_FN) - { - frame->ip = ((ObjFn*)fn)->bytecode; - } - else - { - frame->ip = ((ObjClosure*)fn)->fn->bytecode; - } + // Initialize the first call frame. + fiber->numFrames = 0; + wrenAppendCallFrame(vm, fiber, fn, fiber->stack); } ObjFn* wrenNewFunction(WrenVM* vm, ObjModule* module, diff --git a/src/vm/wren_value.h b/src/vm/wren_value.h index aa049969..69dac7b3 100644 --- a/src/vm/wren_value.h +++ b/src/vm/wren_value.h @@ -41,9 +41,8 @@ // The representation is controlled by the `WREN_NAN_TAGGING` define. If that's // defined, Nan tagging is used. -// TODO: Make these externally controllable. +// TODO: Make this externally controllable. #define STACK_SIZE 1024 -#define MAX_CALL_FRAMES 256 // These macros cast a Value to one of the specific value types. These do *not* // perform any validation, so must only be used after the Value has been @@ -211,8 +210,15 @@ typedef struct sObjFiber Value stack[STACK_SIZE]; Value* stackTop; - CallFrame frames[MAX_CALL_FRAMES]; + // The stack of call frames. This is a dynamic array that grows as needed but + // never shrinks. + CallFrame* frames; + + // The number of frames currently in use in [frames]. int numFrames; + + // The number of [frames] allocated. + int frameCapacity; // Pointer to the first node in the linked list of open upvalues that are // pointing to values still on the stack. The head of the list will be the @@ -620,7 +626,28 @@ ObjClosure* wrenNewClosure(WrenVM* vm, ObjFn* fn); ObjFiber* wrenNewFiber(WrenVM* vm, Obj* fn); // Resets [fiber] back to an initial state where it is ready to invoke [fn]. -void wrenResetFiber(ObjFiber* fiber, Obj* fn); +void wrenResetFiber(WrenVM* vm, ObjFiber* fiber, Obj* fn); + +// Adds a new [CallFrame] to [fiber] invoking [function] whose stack starts at +// [stackStart]. +static inline void wrenAppendCallFrame(WrenVM* vm, ObjFiber* fiber, + Obj* function, Value* stackStart) +{ + // The caller should have ensured we already have enough capacity. + ASSERT(fiber->frameCapacity > fiber->numFrames, "No memory for call frame."); + + CallFrame* frame = &fiber->frames[fiber->numFrames++]; + frame->stackStart = stackStart; + frame->fn = function; + if (function->type == OBJ_FN) + { + frame->ip = ((ObjFn*)function)->bytecode; + } + else + { + frame->ip = ((ObjClosure*)function)->fn->bytecode; + } +} // TODO: The argument list here is getting a bit gratuitous. // Creates a new function object with the given code and constants. The new diff --git a/src/vm/wren_vm.c b/src/vm/wren_vm.c index 5735f815..f8477712 100644 --- a/src/vm/wren_vm.c +++ b/src/vm/wren_vm.c @@ -425,21 +425,22 @@ static Value methodNotFound(WrenVM* vm, ObjClass* classObj, int symbol) // Pushes [function] onto [fiber]'s callstack and invokes it. Expects [numArgs] // arguments (including the receiver) to be on the top of the stack already. // [function] can be an `ObjFn` or `ObjClosure`. -static inline void callFunction(ObjFiber* fiber, Obj* function, int numArgs) +static inline void callFunction( + WrenVM* vm, ObjFiber* fiber, Obj* function, int numArgs) { - // TODO: Check for stack overflow. - CallFrame* frame = &fiber->frames[fiber->numFrames++]; - frame->fn = function; - frame->stackStart = fiber->stackTop - numArgs; + if (fiber->numFrames + 1 > fiber->frameCapacity) + { + int max = fiber->frameCapacity * 2; + fiber->frames = (CallFrame*)wrenReallocate(vm, fiber->frames, + sizeof(CallFrame) * fiber->frameCapacity, + sizeof(CallFrame) * max); + fiber->frameCapacity = max; + } + + // TODO: Check for stack overflow. We handle the call frame array growing, + // but not the stack itself. - if (function->type == OBJ_FN) - { - frame->ip = ((ObjFn*)function)->bytecode; - } - else - { - frame->ip = ((ObjClosure*)function)->fn->bytecode; - } + wrenAppendCallFrame(vm, fiber, function, fiber->stackTop - numArgs); } // Looks up the previously loaded module with [name]. @@ -827,7 +828,7 @@ static WrenInterpretResult runInterpreter(WrenVM* vm, register ObjFiber* fiber) case PRIM_CALL: STORE_FRAME(); - callFunction(fiber, AS_OBJ(args[0]), numArgs); + callFunction(vm, fiber, AS_OBJ(args[0]), numArgs); LOAD_FRAME(); break; @@ -851,7 +852,7 @@ static WrenInterpretResult runInterpreter(WrenVM* vm, register ObjFiber* fiber) case METHOD_BLOCK: STORE_FRAME(); - callFunction(fiber, method->fn.obj, numArgs); + callFunction(vm, fiber, method->fn.obj, numArgs); LOAD_FRAME(); break; @@ -1274,7 +1275,7 @@ void wrenCall(WrenVM* vm, WrenMethod* method, const char* argTypes, ...) runInterpreter(vm, method->fiber); // Reset the fiber to get ready for the next call. - wrenResetFiber(method->fiber, fn); + wrenResetFiber(vm, method->fiber, fn); // Push the receiver back on the stack. *method->fiber->stackTop++ = receiver; diff --git a/test/benchmark/fibers.wren b/test/benchmark/fibers.wren new file mode 100644 index 00000000..0094e601 --- /dev/null +++ b/test/benchmark/fibers.wren @@ -0,0 +1,16 @@ +// Creates 10000 fibers. Each one calls the next in a chain until the last. +var fibers = [] +var sum = 0 + +var start = IO.clock + +for (i in 0...100000) { + fibers.add(new Fiber { + sum = sum + i + if (i < 99999) fibers[i + 1].call() + }) +} + +fibers[0].call() +IO.print(sum) +IO.print("elapsed: ", IO.clock - start)