From f992b824979708ea449c57444a5c01ca253debcc Mon Sep 17 00:00:00 2001 From: howsohazard <143410553+howsohazard@users.noreply.github.com> Date: Thu, 4 Jan 2024 09:33:54 -0500 Subject: [PATCH] 18843: Improves efficiency of assign and accum opcodes and fixes issues with concurrent modifications to stack variables (#52) --- .../evaluablenode/EvaluableNodeManagement.h | 2 +- src/Amalgam/interpreter/Interpreter.cpp | 29 ++- src/Amalgam/interpreter/Interpreter.h | 102 ++++++--- .../interpreter/InterpreterDebugger.cpp | 2 +- .../interpreter/InterpreterOpcodesBase.cpp | 215 ++++++++++-------- .../InterpreterOpcodesDataTypes.cpp | 27 ++- 6 files changed, 230 insertions(+), 147 deletions(-) diff --git a/src/Amalgam/evaluablenode/EvaluableNodeManagement.h b/src/Amalgam/evaluablenode/EvaluableNodeManagement.h index 118d8f56..cf4ba65b 100644 --- a/src/Amalgam/evaluablenode/EvaluableNodeManagement.h +++ b/src/Amalgam/evaluablenode/EvaluableNodeManagement.h @@ -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 diff --git a/src/Amalgam/interpreter/Interpreter.cpp b/src/Amalgam/interpreter/Interpreter.cpp index 1fd9bcff..3b5b0918 100644 --- a/src/Amalgam/interpreter/Interpreter.cpp +++ b/src/Amalgam/interpreter/Interpreter.cpp @@ -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 *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, @@ -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 @@ -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]; @@ -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--) @@ -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) @@ -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(); diff --git a/src/Amalgam/interpreter/Interpreter.h b/src/Amalgam/interpreter/Interpreter.h index 4574dfe9..96964afd 100644 --- a/src/Amalgam/interpreter/Interpreter.h +++ b/src/Amalgam/interpreter/Interpreter.h @@ -64,7 +64,7 @@ class Interpreter EvaluableNode *call_stack = nullptr, EvaluableNode *interpreter_node_stack = nullptr, EvaluableNode *construction_stack = nullptr, std::vector *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, @@ -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)) @@ -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(); @@ -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; @@ -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() @@ -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 @@ -523,7 +535,7 @@ class Interpreter enm->AllocListNode(parentInterpreter->interpreterNodeStackNodes), construction_stack, &csiau, - GetCallStackWriteMutex()); + GetCallStackMutex()); enm->KeepNodeReference(result); @@ -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 @@ -593,7 +605,7 @@ class Interpreter std::vector> 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 @@ -608,6 +620,30 @@ class Interpreter //returns true if it is able to interpret the nodes concurrently bool InterpretEvaluableNodesConcurrently(EvaluableNode *parent_node, std::vector &nodes, std::vector &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 + 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 @@ -621,7 +657,7 @@ class Interpreter return false; #ifdef MULTITHREAD_SUPPORT - if(cur_interpreter->callStackSharedAccessStartingDepth > 0) + if(cur_interpreter->callStackUniqueAccessStartingDepth > 0) return false; #endif } @@ -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 *callStackNodes; - //A stack (list) of the current nodes being executed + //a stack (list) of the current nodes being executed std::vector *interpreterNodeStackNodes; - //The current construction stack, containing an interleaved array of nodes + //the current construction stack, containing an interleaved array of nodes std::vector *constructionStackNodes; //current index for each level of constructionStackNodes; @@ -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; diff --git a/src/Amalgam/interpreter/InterpreterDebugger.cpp b/src/Amalgam/interpreter/InterpreterDebugger.cpp index bcb8dd67..4e954ce0 100644 --- a/src/Amalgam/interpreter/InterpreterDebugger.cpp +++ b/src/Amalgam/interpreter/InterpreterDebugger.cpp @@ -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; diff --git a/src/Amalgam/interpreter/InterpreterOpcodesBase.cpp b/src/Amalgam/interpreter/InterpreterOpcodesBase.cpp index c6dd6ccc..c1662b60 100644 --- a/src/Amalgam/interpreter/InterpreterOpcodesBase.cpp +++ b/src/Amalgam/interpreter/InterpreterOpcodesBase.cpp @@ -9,6 +9,7 @@ #include "EntityManipulation.h" #include "EntityQueries.h" #include "EntityWriteListener.h" +#include "EvaluableNodeManagement.h" #include "EvaluableNodeTreeFunctions.h" #include "PerformanceProfiler.h" @@ -361,7 +362,7 @@ EvaluableNodeReference Interpreter::InterpretNode_ENT_PARALLEL(EvaluableNode *en evaluableNodeManager->AllocListNode(interpreterNodeStackNodes), evaluableNodeManager->AllocListNode(constructionStackNodes), &constructionStackIndicesAndUniqueness, - concurrency_manager.GetCallStackWriteMutex()); + concurrency_manager.GetCallStackMutex()); interpreter.evaluableNodeManager->FreeNodeTreeIfPossible(result); result = EvaluableNodeReference::Null(); @@ -455,18 +456,18 @@ EvaluableNodeReference Interpreter::InterpretNode_ENT_CALL(EvaluableNode *en, bo if(_label_profiling_enabled && function->GetNumLabels() > 0) PerformanceProfiler::StartOperation(function->GetLabel(0), evaluableNodeManager->GetNumberOfUsedNodes()); - //if have an execution context of variables specified, then use it + //if have an call stack context of variables specified, then use it EvaluableNodeReference new_context = EvaluableNodeReference::Null(); if(en->GetOrderedChildNodes().size() > 1) new_context = InterpretNodeForImmediateUse(ocn[1]); - PushNewExecutionContext(new_context); + PushNewCallStack(new_context); //call the code auto retval = InterpretNode(function, immediate_result); //all finished with new context, but can't free it in case returning something - PopExecutionContext(); + PopCallStack(); if(_label_profiling_enabled && function->GetNumLabels() > 0) PerformanceProfiler::EndOperation(evaluableNodeManager->GetNumberOfUsedNodes()); @@ -510,12 +511,12 @@ EvaluableNodeReference Interpreter::InterpretNode_ENT_CALL_SANDBOXED(EvaluableNo if(_label_profiling_enabled && function->GetNumLabels() > 0) PerformanceProfiler::StartOperation(function->GetLabel(0), evaluableNodeManager->GetNumberOfUsedNodes()); - //if have an execution context of variables specified, then use it + //if have a call stack context of variables specified, then use it EvaluableNodeReference args = EvaluableNodeReference::Null(); if(en->GetOrderedChildNodes().size() > 1) args = InterpretNode(ocn[1]); - //build execution context from parameters + //build call stack from parameters EvaluableNodeReference call_stack = ConvertArgsToCallStack(args, *evaluableNodeManager); node_stack.PushEvaluableNode(call_stack); @@ -639,7 +640,7 @@ EvaluableNodeReference Interpreter::InterpretNode_ENT_LET(EvaluableNode *en, boo //add new context auto new_context = InterpretNodeForImmediateUse(ocn[0]); - PushNewExecutionContext(new_context); + PushNewCallStack(new_context); //run code EvaluableNodeReference result = EvaluableNodeReference::Null(); @@ -647,7 +648,7 @@ EvaluableNodeReference Interpreter::InterpretNode_ENT_LET(EvaluableNode *en, boo { if(!result.IsImmediateValue() && result != nullptr && result->GetType() == ENT_CONCLUDE) { - PopExecutionContext(); + PopCallStack(); return RemoveConcludeFromConclusion(result, evaluableNodeManager); } @@ -657,7 +658,7 @@ EvaluableNodeReference Interpreter::InterpretNode_ENT_LET(EvaluableNode *en, boo } //all finished with new context, but can't free it in case returning something - PopExecutionContext(); + PopCallStack(); return result; } @@ -670,10 +671,10 @@ EvaluableNodeReference Interpreter::InterpretNode_ENT_DECLARE(EvaluableNode *en, return EvaluableNodeReference::Null(); //get the current layer of the stack - EvaluableNode *scope = GetCurrentExecutionContext(); + EvaluableNode *scope = GetCurrentCallStackContext(); if(scope == nullptr) //this shouldn't happen, but just in case it does return EvaluableNodeReference::Null(); - auto &scope_ocn = scope->GetMappedChildNodesReference(); + auto &scope_mcn = scope->GetMappedChildNodesReference(); //work on the node that is declaring the variables EvaluableNode *required_vars_node = ocn[0]; @@ -704,8 +705,14 @@ EvaluableNodeReference Interpreter::InterpretNode_ENT_DECLARE(EvaluableNode *en, { if(need_to_interpret && cn != nullptr && !cn->GetIsIdempotent()) { + #ifdef MULTITHREAD_SUPPORT + Concurrency::WriteLock write_lock; + if(callStackMutex != nullptr && GetCallStackDepth() < callStackUniqueAccessStartingDepth) + LockWithoutBlockingGarbageCollection(*callStackMutex, write_lock, required_vars); + #endif + //don't need to do anything if the variable already exists - if(scope_ocn.find(cn_id) != end(scope_ocn)) + if(scope_mcn.find(cn_id) != end(scope_mcn)) continue; PushNewConstructionContext(required_vars, required_vars, EvaluableNodeImmediateValueWithType(cn_id), nullptr); @@ -716,6 +723,12 @@ EvaluableNodeReference Interpreter::InterpretNode_ENT_DECLARE(EvaluableNode *en, } else //just insert if it doesn't exist { + #ifdef MULTITHREAD_SUPPORT + Concurrency::WriteLock write_lock; + if(callStackMutex != nullptr && GetCallStackDepth() < callStackUniqueAccessStartingDepth) + LockWithoutBlockingGarbageCollection(*callStackMutex, write_lock, required_vars); + #endif + auto [inserted, node_ptr] = scope->SetMappedChildNode(cn_id, cn, false); if(!inserted) { @@ -808,31 +821,22 @@ EvaluableNodeReference Interpreter::InterpretNode_ENT_ASSIGN_and_ACCUM(Evaluable //retrieve the symbol size_t destination_call_stack_index = 0; + EvaluableNode **value_destination = nullptr; #ifdef MULTITHREAD_SUPPORT - //if editing a shared variable, then need to reserve the stack and re-retrieve the symbol - //note that the above call to GetOrCreateExecutionContextSymbol *is* safe with multithreading, because it will only - //modify the stack that the interpreter has unique access to, but if it returns something further up the stack, - //then it will only read and will return a pointer which could be invalid, but will obtain the pointer again below - //after the lock making sure that it is valid - GetExecutionContextSymbolLocation(variable_sid, destination_call_stack_index); - Concurrency::SingleLock lock(*callStackWriteMutex, std::defer_lock); - if(destination_call_stack_index < callStackSharedAccessStartingDepth && callStackWriteMutex != nullptr) - { - //just in case more than one instruction is trying to write at the same time, - // but one is blocking for garbage collection, - // keep checking until it can get the lock - while(!lock.try_lock()) - { - //keep the value in case collect garbage - node_stack.PushEvaluableNode(variable_value_node); - CollectGarbage(); - node_stack.PopEvaluableNode(); - } - } + //attempt to get location, but only attempt locations unique to this thread + value_destination = GetCallStackSymbolLocation(variable_sid, destination_call_stack_index, true, false); + //if editing a shared variable, need to see if it is in a shared region of the stack, + // need a write lock to the stack and variable + Concurrency::WriteLock write_lock; + if(callStackMutex != nullptr && value_destination == nullptr) + LockWithoutBlockingGarbageCollection(*callStackMutex, write_lock, variable_value_node); #endif - EvaluableNode **value_destination = GetOrCreateExecutionContextSymbolLocation(variable_sid, destination_call_stack_index); + //in single threaded, this will just be true + //in multithreaded, if variable was not found, then may need to create it + if(value_destination == nullptr) + value_destination = GetOrCreateCallStackSymbolLocation(variable_sid, destination_call_stack_index); if(accum) { @@ -842,7 +846,7 @@ EvaluableNodeReference Interpreter::InterpretNode_ENT_ASSIGN_and_ACCUM(Evaluable #ifdef MULTITHREAD_SUPPORT //if editing a shared variable, then need to make a copy before editing in place to prevent another thread from reading the data structure mid-edit - if(destination_call_stack_index < callStackSharedAccessStartingDepth) + if(destination_call_stack_index < callStackUniqueAccessStartingDepth) value_destination_node = evaluableNodeManager->DeepAllocCopy(value_destination_node); #endif @@ -868,27 +872,22 @@ EvaluableNodeReference Interpreter::InterpretNode_ENT_ASSIGN_and_ACCUM(Evaluable //retrieve the symbol size_t destination_call_stack_index = 0; + EvaluableNode **value_destination = nullptr; #ifdef MULTITHREAD_SUPPORT - //if editing a shared variable, then need to reserve the stack and re-retrieve the symbol - GetExecutionContextSymbolLocation(variable_sid, destination_call_stack_index); - - Concurrency::SingleLock lock(*callStackWriteMutex, std::defer_lock); - if(destination_call_stack_index < callStackSharedAccessStartingDepth && callStackWriteMutex != nullptr) - { - //just in case more than one instruction is trying to write at the same time, - // but one is blocking for garbage collection, - // keep checking until it can get the lock - while(!lock.try_lock()) - { - //keep the value in case collect garbage - auto node_stack = CreateInterpreterNodeStackStateSaver(new_value); - CollectGarbage(); - } - } + //attempt to get location, but only attempt locations unique to this thread + value_destination = GetCallStackSymbolLocation(variable_sid, destination_call_stack_index, true, false); + //if editing a shared variable, need to see if it is in a shared region of the stack, + // need a write lock to the stack and variable + Concurrency::WriteLock write_lock; + if(callStackMutex != nullptr && value_destination == nullptr) + LockWithoutBlockingGarbageCollection(*callStackMutex, write_lock, new_value); #endif - EvaluableNode **value_destination = GetOrCreateExecutionContextSymbolLocation(variable_sid, destination_call_stack_index); + //in single threaded, this will just be true + //in multithreaded, if variable was not found, then may need to create it + if(value_destination == nullptr) + value_destination = GetOrCreateCallStackSymbolLocation(variable_sid, destination_call_stack_index); if(accum) { @@ -898,7 +897,7 @@ EvaluableNodeReference Interpreter::InterpretNode_ENT_ASSIGN_and_ACCUM(Evaluable #ifdef MULTITHREAD_SUPPORT //if editing a shared variable, then need to make a copy before editing in place to prevent another thread from reading the data structure mid-edit - if(destination_call_stack_index < callStackSharedAccessStartingDepth) + if(destination_call_stack_index < callStackUniqueAccessStartingDepth) value_destination_node = evaluableNodeManager->DeepAllocCopy(value_destination_node); #endif @@ -914,49 +913,59 @@ EvaluableNodeReference Interpreter::InterpretNode_ENT_ASSIGN_and_ACCUM(Evaluable } else //more than 2, need to make a copy and fill in as appropriate { + //obtain all of the edits to make the edits transactionally at once when all are collected + auto node_stack = CreateInterpreterNodeStackStateSaver(); + auto &replacements = *node_stack.stack; + size_t replacements_start_index = node_stack.originalStackSize; + + //keeps track of whether each address is unique so they can be freed if relevant + std::vector is_address_unique; + //get each address/value pair to replace in result - size_t replace_change_index = 1; - for(; replace_change_index + 1 < num_params; replace_change_index += 2) + for(size_t ocn_index = 1; ocn_index + 1 < num_params; ocn_index += 2) { if(AreExecutionResourcesExhausted()) return EvaluableNodeReference::Null(); - auto new_value = InterpretNodeForImmediateUse(ocn[replace_change_index + 1]); - auto node_stack = CreateInterpreterNodeStackStateSaver(new_value); + auto address = InterpretNodeForImmediateUse(ocn[ocn_index]); + node_stack.PushEvaluableNode(address); + is_address_unique.push_back(address.unique); + auto new_value = InterpretNodeForImmediateUse(ocn[ocn_index + 1]); + node_stack.PushEvaluableNode(new_value); + } + size_t num_replacements = (num_params - 1) / 2; - EvaluableNodeReference address_list_node = InterpretNodeForImmediateUse(ocn[replace_change_index]); + //retrieve the symbol + size_t destination_call_stack_index = 0; + EvaluableNode **value_destination = nullptr; - //retrieve the symbol - size_t destination_call_stack_index = 0; + #ifdef MULTITHREAD_SUPPORT + //attempt to get location, but only attempt locations unique to this thread + value_destination = GetCallStackSymbolLocation(variable_sid, destination_call_stack_index, true, false); + //if editing a shared variable, need to see if it is in a shared region of the stack, + // need a write lock to the stack and variable + Concurrency::WriteLock write_lock; + if(callStackMutex != nullptr && value_destination == nullptr) + LockWithoutBlockingGarbageCollection(*callStackMutex, write_lock); + #endif - #ifdef MULTITHREAD_SUPPORT - //if editing a shared variable, then need to reserve the stack and re-retrieve the symbol - GetExecutionContextSymbolLocation(variable_sid, destination_call_stack_index); + //in single threaded, this will just be true + //in multithreaded, if variable was not found, then may need to create it + if(value_destination == nullptr) + value_destination = GetOrCreateCallStackSymbolLocation(variable_sid, destination_call_stack_index); - Concurrency::SingleLock lock(*callStackWriteMutex, std::defer_lock); - if(destination_call_stack_index < callStackSharedAccessStartingDepth && callStackWriteMutex != nullptr) - { - //just in case more than one instruction is trying to write at the same time, - // but one is blocking for garbage collection, - // keep checking until it can get the lock - while(!lock.try_lock()) - { - node_stack.PushEvaluableNode(address_list_node); - CollectGarbage(); - node_stack.PopEvaluableNode(); - } - } - #endif - - EvaluableNode **value_destination = GetOrCreateExecutionContextSymbolLocation(variable_sid, destination_call_stack_index); + //need to make a copy so that modifications can be dropped in directly + // this is essential as some values may be shared by other areas of memory, threads, or entities + EvaluableNode *value_replacement = evaluableNodeManager->DeepAllocCopy(*value_destination); - //need to make a copy so that it can be dropped in directly - // this is essential as some values may be complex data structures from other entities - EvaluableNode *value_replacement = evaluableNodeManager->DeepAllocCopy(*value_destination); + for(size_t index = 0; index < num_replacements; index++) + { + EvaluableNodeReference address(replacements[replacements_start_index + 2 * index], is_address_unique[index]); + EvaluableNodeReference new_value(replacements[replacements_start_index + 2 * index + 1], false); //find location to store results - EvaluableNode **copy_destination = TraverseToDestinationFromTraversalPathList(&value_replacement, address_list_node, true); - evaluableNodeManager->FreeNodeTreeIfPossible(address_list_node); + EvaluableNode **copy_destination = TraverseToDestinationFromTraversalPathList(&value_replacement, address, true); + evaluableNodeManager->FreeNodeTreeIfPossible(address); if(copy_destination == nullptr) continue; @@ -975,9 +984,9 @@ EvaluableNodeReference Interpreter::InterpretNode_ENT_ASSIGN_and_ACCUM(Evaluable { *copy_destination = new_value; } - - *value_destination = value_replacement; } + + *value_destination = value_replacement; } string_intern_pool.DestroyStringReference(variable_sid); @@ -993,12 +1002,19 @@ EvaluableNodeReference Interpreter::InterpretNode_ENT_RETRIEVE(EvaluableNode *en auto to_lookup = InterpretNodeForImmediateUse(ocn[0]); +#ifdef MULTITHREAD_SUPPORT + //accessing everything in the stack, so need exclusive access + Concurrency::ReadLock lock; + if(callStackMutex != nullptr) + LockWithoutBlockingGarbageCollection(*callStackMutex, lock); +#endif + //get the value(s) if(to_lookup == nullptr || IsEvaluableNodeTypeImmediate(to_lookup->GetType())) { StringInternPool::StringID symbol_name_sid = EvaluableNode::ToStringIDIfExists(to_lookup); evaluableNodeManager->FreeNodeTreeIfPossible(to_lookup); - return EvaluableNodeReference(GetExecutionContextSymbol(symbol_name_sid), false); + return EvaluableNodeReference(GetCallStackSymbol(symbol_name_sid), false); } else if(to_lookup->IsAssociativeArray()) { @@ -1012,7 +1028,7 @@ EvaluableNodeReference Interpreter::InterpretNode_ENT_RETRIEVE(EvaluableNode *en EvaluableNodeReference cnr(cn, to_lookup.unique); evaluableNodeManager->FreeNodeTreeIfPossible(cnr); - cn = GetExecutionContextSymbol(cn_id); + cn = GetCallStackSymbol(cn_id); } return EvaluableNodeReference(to_lookup, false); @@ -1035,7 +1051,7 @@ EvaluableNodeReference Interpreter::InterpretNode_ENT_RETRIEVE(EvaluableNode *en EvaluableNodeReference cnr(cn, to_lookup.unique); evaluableNodeManager->FreeNodeTreeIfPossible(cnr); - cn = GetExecutionContextSymbol(symbol_name_sid); + cn = GetCallStackSymbol(symbol_name_sid); } return EvaluableNodeReference(to_lookup, false); @@ -1289,16 +1305,9 @@ EvaluableNodeReference Interpreter::InterpretNode_ENT_PREVIOUS_RESULT(EvaluableN EvaluableNodeReference Interpreter::InterpretNode_ENT_STACK(EvaluableNode *en, bool immediate_result) { #ifdef MULTITHREAD_SUPPORT - //accessing everything in the stack, so need exclusive access - Concurrency::SingleLock lock(*callStackWriteMutex, std::defer_lock); - if(callStackWriteMutex != nullptr) - { - //just in case more than one instruction is trying to write at the same time, - // but one is blocking for garbage collection, - // keep checking until it can get the lock - while(!lock.try_lock()) - CollectGarbage(); - } + Concurrency::ReadLock lock; + if(callStackMutex != nullptr) + LockWithoutBlockingGarbageCollection(*callStackMutex, lock); #endif //can create this node on the stack because will be making a copy @@ -1320,7 +1329,15 @@ EvaluableNodeReference Interpreter::InterpretNode_ENT_ARGS(EvaluableNode *en, bo //make sure have a large enough stack if(callStackNodes->size() >= depth + 1) - return EvaluableNodeReference( (*callStackNodes)[callStackNodes->size() - (depth + 1)], false); //0 index is top of stack + { + #ifdef MULTITHREAD_SUPPORT + Concurrency::ReadLock lock; + if(callStackMutex != nullptr && GetCallStackDepth() < callStackUniqueAccessStartingDepth) + LockWithoutBlockingGarbageCollection(*callStackMutex, lock); + #endif + + return EvaluableNodeReference((*callStackNodes)[callStackNodes->size() - (depth + 1)], false); //0 index is top of stack + } else return EvaluableNodeReference::Null(); } diff --git a/src/Amalgam/interpreter/InterpreterOpcodesDataTypes.cpp b/src/Amalgam/interpreter/InterpreterOpcodesDataTypes.cpp index b0376460..4d232ce9 100644 --- a/src/Amalgam/interpreter/InterpreterOpcodesDataTypes.cpp +++ b/src/Amalgam/interpreter/InterpreterOpcodesDataTypes.cpp @@ -196,9 +196,30 @@ EvaluableNodeReference Interpreter::InterpretNode_ENT_SYMBOL(EvaluableNode *en, if(sid == StringInternPool::NOT_A_STRING_ID) return EvaluableNodeReference::Null(); - EvaluableNodeReference value(GetExecutionContextSymbol(sid), false); - if(value != nullptr) - return value; +#ifdef MULTITHREAD_SUPPORT + if(callStackMutex != nullptr) + { + //first check unique + size_t call_stack_index = 0; + EvaluableNode **value_ptr = GetCallStackSymbolLocation(sid, call_stack_index, true, false); + if(value_ptr != nullptr) + return EvaluableNodeReference(*value_ptr, false); + + Concurrency::ReadLock lock; + LockWithoutBlockingGarbageCollection(*callStackMutex, lock); + + //now check shared + value_ptr = GetCallStackSymbolLocation(sid, call_stack_index, false, true); + if(value_ptr != nullptr) + return EvaluableNodeReference(*value_ptr, false); + } + else //no multithreading currently happening +#endif + { + EvaluableNodeReference value(GetCallStackSymbol(sid), false); + if(value != nullptr) + return value; + } //if didn't find it in the stack, try it in the labels EntityReadReference cur_entity_ref(curEntity);