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)