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.
This commit is contained in:
Bob Nystrom
2015-07-01 00:00:25 -07:00
parent 18dcd3ce3d
commit 2387d4dc31
5 changed files with 82 additions and 36 deletions

View File

@ -67,6 +67,8 @@ BENCHMARK("fib", r"""317811
317811
317811""")
BENCHMARK("fibers", r"""4999950000""")
BENCHMARK("for", r"""499999500000""")
BENCHMARK("method_call", r"""true

View File

@ -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,

View File

@ -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

View File

@ -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;

View File

@ -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)