Skip to content

Commit

Permalink
18843: Improves efficiency of assign and accum opcodes and fixes issu…
Browse files Browse the repository at this point in the history
…es with concurrent modifications to stack variables (#52)
  • Loading branch information
howsohazard authored Jan 4, 2024
1 parent 008bf05 commit f992b82
Show file tree
Hide file tree
Showing 6 changed files with 230 additions and 147 deletions.
2 changes: 1 addition & 1 deletion src/Amalgam/evaluablenode/EvaluableNodeManagement.h
Original file line number Diff line number Diff line change
Expand Up @@ -685,7 +685,7 @@ class EvaluableNodeManager
{
#ifdef MULTITHREAD_SUPPORT
//this is much more expensive with multithreading, so only do when useful
if((executionCyclesSinceLastGarbageCollection & 511) != 0)
if((executionCyclesSinceLastGarbageCollection & 16383) != 0)
return;

//be opportunistic and only attempt to reclaim if it can grab a write lock
Expand Down
29 changes: 20 additions & 9 deletions src/Amalgam/interpreter/Interpreter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ Interpreter::Interpreter(EvaluableNodeManager *enm,
EvaluableNodeReference Interpreter::ExecuteNode(EvaluableNode *en,
EvaluableNode *call_stack, EvaluableNode *interpreter_node_stack,
EvaluableNode *construction_stack, std::vector<ConstructionStackIndexAndPreviousResultUniqueness> *construction_stack_indices,
Concurrency::SingleMutex *call_stack_write_mutex)
Concurrency::ReadWriteMutex *call_stack_write_mutex)
#else
EvaluableNodeReference Interpreter::ExecuteNode(EvaluableNode *en,
EvaluableNode *call_stack, EvaluableNode *interpreter_node_stack,
Expand All @@ -347,11 +347,11 @@ EvaluableNodeReference Interpreter::ExecuteNode(EvaluableNode *en,

#ifdef MULTITHREAD_SUPPORT
if(call_stack == nullptr)
callStackSharedAccessStartingDepth = 0;
callStackUniqueAccessStartingDepth = 0;
else
callStackSharedAccessStartingDepth = call_stack->GetOrderedChildNodes().size();
callStackUniqueAccessStartingDepth = call_stack->GetOrderedChildNodes().size();

callStackWriteMutex = call_stack_write_mutex;
callStackMutex = call_stack_write_mutex;
#endif

//use specified or create new callStack
Expand Down Expand Up @@ -420,10 +420,21 @@ EvaluableNodeReference Interpreter::ConvertArgsToCallStack(EvaluableNodeReferenc
return EvaluableNodeReference(call_stack, args.unique);
}

EvaluableNode **Interpreter::GetExecutionContextSymbolLocation(const StringInternPool::StringID symbol_sid, size_t &call_stack_index)
EvaluableNode **Interpreter::GetCallStackSymbolLocation(const StringInternPool::StringID symbol_sid, size_t &call_stack_index
#ifdef MULTITHREAD_SUPPORT
, bool include_unique_access, bool include_shared_access
#endif
)
{
#ifdef MULTITHREAD_SUPPORT
size_t highest_index = (include_unique_access ? callStackNodes->size() : callStackUniqueAccessStartingDepth);
size_t lowest_index = (include_shared_access ? 0 : callStackUniqueAccessStartingDepth);
#else
size_t highest_index = callStackNodes->size();
size_t lowest_index = 0;
#endif
//find symbol by walking up the stack; each layer must be an assoc
for(call_stack_index = callStackNodes->size(); call_stack_index > 0; call_stack_index--)
for(call_stack_index = highest_index; call_stack_index > lowest_index; call_stack_index--)
{
EvaluableNode *cur_context = (*callStackNodes)[call_stack_index - 1];

Expand All @@ -444,7 +455,7 @@ EvaluableNode **Interpreter::GetExecutionContextSymbolLocation(const StringInter
return nullptr;
}

EvaluableNode **Interpreter::GetOrCreateExecutionContextSymbolLocation(const StringInternPool::StringID symbol_sid, size_t &call_stack_index)
EvaluableNode **Interpreter::GetOrCreateCallStackSymbolLocation(const StringInternPool::StringID symbol_sid, size_t &call_stack_index)
{
//find appropriate context for symbol by walking up the stack
for(call_stack_index = callStackNodes->size(); call_stack_index > 0; call_stack_index--)
Expand Down Expand Up @@ -520,7 +531,7 @@ EvaluableNodeReference Interpreter::InterpretNode(EvaluableNode *en, bool immedi
return retval;
}

EvaluableNode *Interpreter::GetCurrentExecutionContext()
EvaluableNode *Interpreter::GetCurrentCallStackContext()
{
//this should not happen, but just in case
if(callStackNodes->size() < 1)
Expand Down Expand Up @@ -798,7 +809,7 @@ bool Interpreter::InterpretEvaluableNodesConcurrently(EvaluableNode *parent_node
evaluableNodeManager->AllocListNode(interpreterNodeStackNodes),
evaluableNodeManager->AllocListNode(constructionStackNodes),
&constructionStackIndicesAndUniqueness,
concurrency_manager.GetCallStackWriteMutex());
concurrency_manager.GetCallStackMutex());

evaluableNodeManager->KeepNodeReference(result);
interpreter.memoryModificationLock.unlock();
Expand Down
102 changes: 68 additions & 34 deletions src/Amalgam/interpreter/Interpreter.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class Interpreter
EvaluableNode *call_stack = nullptr, EvaluableNode *interpreter_node_stack = nullptr,
EvaluableNode *construction_stack = nullptr,
std::vector<ConstructionStackIndexAndPreviousResultUniqueness> *construction_stack_indices = nullptr,
Concurrency::SingleMutex *call_stack_write_mutex = nullptr);
Concurrency::ReadWriteMutex *call_stack_write_mutex = nullptr);
#else
EvaluableNodeReference ExecuteNode(EvaluableNode *en,
EvaluableNode *call_stack = nullptr, EvaluableNode *interpreter_node_stack = nullptr,
Expand Down Expand Up @@ -103,7 +103,7 @@ class Interpreter

//pushes new_context on the stack; new_context should be a unique associative array,
// but if not, it will attempt to put an appropriate unique associative array on callStackNodes
__forceinline void PushNewExecutionContext(EvaluableNodeReference new_context)
__forceinline void PushNewCallStack(EvaluableNodeReference new_context)
{
//make sure unique assoc
if(EvaluableNode::IsAssociativeArray(new_context))
Expand All @@ -123,8 +123,8 @@ class Interpreter
callStackNodes->push_back(new_context);
}

//pops the top execution context off the stack
__forceinline void PopExecutionContext()
//pops the top context off the stack
__forceinline void PopCallStack()
{
if(callStackNodes->size() >= 1)
callStackNodes->pop_back();
Expand Down Expand Up @@ -228,22 +228,28 @@ class Interpreter
entry.unique = false;
}

//Makes sure that args is an active associative array is proper for execution context, meaning initialized assoc and a unique reference.
//Makes sure that args is an active associative array is proper for context, meaning initialized assoc and a unique reference.
// Will allocate a new node appropriately if it is not
//Then wraps the args on a list which will form the execution context stack and returns that
//Then wraps the args on a list which will form the call stack and returns that
//ensures that args is still a valid EvaluableNodeReference after the call
static EvaluableNodeReference ConvertArgsToCallStack(EvaluableNodeReference args, EvaluableNodeManager &enm);

//finds a pointer to the location of the symbol's pointer to value in the top of the context stack and returns a pointer to the location of the symbol's pointer to value,
// nullptr if it does not exist
// also sets call_stack_index to the level in the call stack that it was found
EvaluableNode **GetExecutionContextSymbolLocation(const StringInternPool::StringID symbol_sid, size_t &call_stack_index);
//if include_unique_access is true, then it will cover the top of the stack to callStackUniqueAccessStartingDepth
//if include_shared_access is true, then it will cover the bottom of the stack from callStackUniqueAccessStartingDepth to 0
EvaluableNode **GetCallStackSymbolLocation(const StringInternPool::StringID symbol_sid, size_t &call_stack_index
#ifdef MULTITHREAD_SUPPORT
, bool include_unique_access = true, bool include_shared_access = true
#endif
);

//like the other type of GetExecutionContextSymbolLocation, but returns the EvaluableNode pointer instead of a pointer-to-a-pointer
__forceinline EvaluableNode *GetExecutionContextSymbol(const StringInternPool::StringID symbol_sid)
//like the other type of GetCallStackSymbolLocation, but returns the EvaluableNode pointer instead of a pointer-to-a-pointer
__forceinline EvaluableNode *GetCallStackSymbol(const StringInternPool::StringID symbol_sid)
{
size_t call_stack_index = 0;
EvaluableNode **en_ptr = GetExecutionContextSymbolLocation(symbol_sid, call_stack_index);
EvaluableNode **en_ptr = GetCallStackSymbolLocation(symbol_sid, call_stack_index);
if(en_ptr == nullptr)
return nullptr;

Expand All @@ -252,7 +258,13 @@ class Interpreter

//finds a pointer to the location of the symbol's pointer to value or creates the symbol in the top of the context stack and returns a pointer to the location of the symbol's pointer to value
// also sets call_stack_index to the level in the call stack that it was found
EvaluableNode **GetOrCreateExecutionContextSymbolLocation(const StringInternPool::StringID symbol_sid, size_t &call_stack_index);
EvaluableNode **GetOrCreateCallStackSymbolLocation(const StringInternPool::StringID symbol_sid, size_t &call_stack_index);

//returns the current call stack index
__forceinline size_t GetCallStackDepth()
{
return callStackNodes->size() - 1;
}

//creates a stack state saver for the interpreterNodeStack, which will be restored back to its previous condition when this object is destructed
__forceinline EvaluableNodeStackStateSaver CreateInterpreterNodeStackStateSaver()
Expand Down Expand Up @@ -292,8 +304,8 @@ class Interpreter
//where to allocate new nodes
EvaluableNodeManager *evaluableNodeManager;

//returns the current execution context, nullptr if none
EvaluableNode *GetCurrentExecutionContext();
//returns the current call stack context, nullptr if none
EvaluableNode *GetCurrentCallStackContext();

//returns an EvaluableNodeReference for value, allocating if necessary based on if immediate result is needed
template<typename T>
Expand Down Expand Up @@ -523,7 +535,7 @@ class Interpreter
enm->AllocListNode(parentInterpreter->interpreterNodeStackNodes),
construction_stack,
&csiau,
GetCallStackWriteMutex());
GetCallStackMutex());

enm->KeepNodeReference(result);

Expand Down Expand Up @@ -576,14 +588,14 @@ class Interpreter
}

//returns the relevant write mutex for the call stack
constexpr Concurrency::SingleMutex *GetCallStackWriteMutex()
constexpr Concurrency::ReadWriteMutex *GetCallStackMutex()
{
//if there is one currently in use, use it
if(parentInterpreter->callStackWriteMutex != nullptr)
return parentInterpreter->callStackWriteMutex;
if(parentInterpreter->callStackMutex != nullptr)
return parentInterpreter->callStackMutex;

//start a new one
return &callStackWriteMutex;
return &callStackMutex;
}

//interpreters run concurrently, the size of numTasks
Expand All @@ -593,7 +605,7 @@ class Interpreter
std::vector<std::future<EvaluableNodeReference>> resultFutures;

//mutex to allow only one thread to write to a call stack symbol at once
Concurrency::SingleMutex callStackWriteMutex;
Concurrency::ReadWriteMutex callStackMutex;

protected:
//interpreter that is running all the concurrent interpreters
Expand All @@ -608,6 +620,30 @@ class Interpreter
//returns true if it is able to interpret the nodes concurrently
bool InterpretEvaluableNodesConcurrently(EvaluableNode *parent_node, std::vector<EvaluableNode *> &nodes, std::vector<EvaluableNodeReference> &interpreted_nodes);

//acquires lock, but does so in a way as to not block other threads that may be waiting on garbage collection
//if en_to_preserve is not null, then it will create a stack saver for it if garbage collection is invoked
template<typename LockType>
inline void LockWithoutBlockingGarbageCollection(
Concurrency::ReadWriteMutex &mutex, LockType &lock, EvaluableNode *en_to_preserve = nullptr)
{
lock = LockType(*callStackMutex, std::defer_lock);
//if there is lock contention, but one is blocking for garbage collection,
// keep checking until it can get the lock
if(en_to_preserve)
{
while(!lock.try_lock())
{
auto node_stack = CreateInterpreterNodeStackStateSaver(en_to_preserve);
CollectGarbage();
}
}
else
{
while(!lock.try_lock())
CollectGarbage();
}
}

#endif

//returns false if this or any calling interpreter is currently running on the entity specified or if there is any active concurrency
Expand All @@ -621,7 +657,7 @@ class Interpreter
return false;

#ifdef MULTITHREAD_SUPPORT
if(cur_interpreter->callStackSharedAccessStartingDepth > 0)
if(cur_interpreter->callStackUniqueAccessStartingDepth > 0)
return false;
#endif
}
Expand Down Expand Up @@ -920,31 +956,31 @@ class Interpreter
//ensures that there are no reachable nodes that are deallocated
void ValidateEvaluableNodeIntegrity();

//Current execution step - number of nodes executed
//current execution step - number of nodes executed
ExecutionCycleCount curExecutionStep;

//Maximum number of execution steps by this Interpreter and anything called from it. If 0, then unlimited.
//Will terminate execution if the value is reached
//maximum number of execution steps by this Interpreter and anything called from it. If 0, then unlimited.
//will terminate execution if the value is reached
ExecutionCycleCount maxNumExecutionSteps;

//Current number of nodes created by this interpreter, to be compared to maxNumExecutionNodes
//current number of nodes created by this interpreter, to be compared to maxNumExecutionNodes
// should be the sum of curNumExecutionNodesAllocatedToEntities plus any temporary nodes
size_t curNumExecutionNodes;

//number of nodes allocated only to entities
size_t curNumExecutionNodesAllocatedToEntities;

//Maximum number of nodes allowed to be allocated by this Interpreter and anything called from it. If 0, then unlimited.
//Will terminate execution if the value is reached
//maximum number of nodes allowed to be allocated by this Interpreter and anything called from it. If 0, then unlimited.
//will terminate execution if the value is reached
size_t maxNumExecutionNodes;

//The current execution context; the call stack
//the call stack is comprised of the variable contexts
std::vector<EvaluableNode *> *callStackNodes;

//A stack (list) of the current nodes being executed
//a stack (list) of the current nodes being executed
std::vector<EvaluableNode *> *interpreterNodeStackNodes;

//The current construction stack, containing an interleaved array of nodes
//the current construction stack, containing an interleaved array of nodes
std::vector<EvaluableNode *> *constructionStackNodes;

//current index for each level of constructionStackNodes;
Expand All @@ -969,12 +1005,10 @@ class Interpreter
protected:

//the depth of the call stack where multiple threads may modify the same variables
size_t callStackSharedAccessStartingDepth;
size_t callStackUniqueAccessStartingDepth;

//pointer to a mutex for writing to shared variables below callStackSharedAccessStartingDepth
//note that reading does not need to be synchronized because the writes are done with regard to pointers,
// which are an atomic operation on every major processor in the world, and even Linux core libraries are built on this assumption
Concurrency::SingleMutex *callStackWriteMutex;
//pointer to a mutex for writing to shared variables below callStackUniqueAccessStartingDepth
Concurrency::ReadWriteMutex *callStackMutex;

//buffer to store read locks for deep locking entities
Concurrency::ReadLocksBuffer entityReadLockBuffer;
Expand Down
2 changes: 1 addition & 1 deletion src/Amalgam/interpreter/InterpreterDebugger.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,7 @@ EvaluableNodeReference Interpreter::InterpretNode_DEBUG(EvaluableNode *en, bool
bool value_exists = true;

size_t call_stack_index = 0;
EvaluableNode **en_ptr = GetExecutionContextSymbolLocation(sid, call_stack_index);
EvaluableNode **en_ptr = GetCallStackSymbolLocation(sid, call_stack_index);
if(en_ptr != nullptr)
{
node = *en_ptr;
Expand Down
Loading

0 comments on commit f992b82

Please sign in to comment.