diff options
Diffstat (limited to 'src/jit/loopcloning.cpp')
-rw-r--r-- | src/jit/loopcloning.cpp | 845 |
1 files changed, 845 insertions, 0 deletions
diff --git a/src/jit/loopcloning.cpp b/src/jit/loopcloning.cpp new file mode 100644 index 0000000000..8ce015e607 --- /dev/null +++ b/src/jit/loopcloning.cpp @@ -0,0 +1,845 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +/*XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +XX XX +XX LoopCloning XX +XX XX +XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +*/ + +#include "jitpch.h" + +//-------------------------------------------------------------------------------------------------- +// ToGenTree - Convert an arrLen operation into a gentree node. +// +// Arguments: +// comp Compiler instance to allocate trees +// +// Return Values: +// Returns the gen tree representation for arrLen or MD Array node as defined by +// the "type" member +// +// Notes: +// This tree produces GT_INDEX node, the caller is supposed to morph it appropriately +// so it can be codegen'ed. +// +GenTreePtr LC_Array::ToGenTree(Compiler* comp) +{ + // If jagged array + if (type == Jagged) + { + // Create a a[i][j][k].length type node. + GenTreePtr arr = comp->gtNewLclvNode(arrIndex->arrLcl, comp->lvaTable[arrIndex->arrLcl].lvType); + int rank = GetDimRank(); + for (int i = 0; i < rank; ++i) + { + arr = comp->gtNewIndexRef(TYP_REF, arr, comp->gtNewLclvNode(arrIndex->indLcls[i], + comp->lvaTable[arrIndex->indLcls[i]].lvType)); + } + // If asked for arrlen invoke arr length operator. + if (oper == ArrLen) + { + GenTreePtr arrLen = new (comp, GT_ARR_LENGTH) GenTreeArrLen(TYP_INT, arr, offsetof(CORINFO_Array, length)); + return arrLen; + } + else + { + assert(oper == None); + return arr; + } + } + else + { + // TODO-CQ: Optimize for MD Array. + assert(!"Optimize for MD Array"); + } + return nullptr; +} + +//-------------------------------------------------------------------------------------------------- +// ToGenTree - Convert an "identifier" into a gentree node. +// +// Arguments: +// comp Compiler instance to allocate trees +// +// Return Values: +// Returns the gen tree representation for either a constant or a variable or an arrLen operation +// defined by the "type" member +// +GenTreePtr LC_Ident::ToGenTree(Compiler* comp) +{ + // Convert to GenTree nodes. + switch (type) + { + case Const: +#ifdef _TARGET_64BIT_ + return comp->gtNewLconNode(constant); +#else + return comp->gtNewIconNode((ssize_t)constant); +#endif + case Var: + return comp->gtNewLclvNode((unsigned)constant, comp->lvaTable[constant].lvType); + case ArrLen: + return arrLen.ToGenTree(comp); + case Null: + return comp->gtNewIconNode(0, TYP_REF); + default: + assert(!"Could not convert LC_Ident to GenTree"); + unreached(); + break; + } +} + +//-------------------------------------------------------------------------------------------------- +// ToGenTree - Convert an "expression" into a gentree node. +// +// Arguments: +// comp Compiler instance to allocate trees +// +// Return Values: +// Returns the gen tree representation for either a constant or a variable or an arrLen operation +// defined by the "type" member +// +GenTreePtr LC_Expr::ToGenTree(Compiler* comp) +{ + // Convert to GenTree nodes. + switch (type) + { + case Ident: + return ident.ToGenTree(comp); + case IdentPlusConst: +#ifdef _TARGET_64BIT_ + return comp->gtNewOperNode(GT_ADD, TYP_LONG, ident.ToGenTree(comp), comp->gtNewLconNode(constant)); +#else + return comp->gtNewOperNode(GT_ADD, TYP_INT, ident.ToGenTree(comp), comp->gtNewIconNode((ssize_t)constant)); +#endif + default: + assert(!"Could not convert LC_Expr to GenTree"); + unreached(); + break; + } +} + +//-------------------------------------------------------------------------------------------------- +// ToGenTree - Convert a "condition" into a gentree node. +// +// Arguments: +// comp Compiler instance to allocate trees +// +// Return Values: +// Returns the gen tree representation for the conditional operator on lhs and rhs trees +// +GenTreePtr LC_Condition::ToGenTree(Compiler* comp) +{ + return comp->gtNewOperNode(oper, TYP_INT, op1.ToGenTree(comp), op2.ToGenTree(comp)); +} + +//-------------------------------------------------------------------------------------------------- +// Evaluates - Evaluate a given loop cloning condition if it can be statically evaluated. +// +// Arguments: +// pResult The evaluation result +// +// Return Values: +// Returns true if the condition can be statically evaluated. If the condition's result +// is statically unknown then return false. In other words, true if "pResult" is valid. +// +bool LC_Condition::Evaluates(bool* pResult) +{ + switch (oper) + { + case GT_EQ: + case GT_GE: + case GT_LE: + // If op1 == op2 then equality should result in true. + if (op1 == op2) + { + *pResult = true; + return true; + } + break; + + case GT_GT: + case GT_LT: + case GT_NE: + // If op1 == op2 then inequality should result in false. + if (op1 == op2) + { + *pResult = false; + return true; + } + break; + + default: + // for all other 'oper' kinds, we will return false + break; + } + return false; +} + +//-------------------------------------------------------------------------------------------------- +// Combines - Check whether two conditions would combine to yield a single new condition. +// +// Arguments: +// cond The condition that is checked if it would combine with "*this" condition. +// newCond The resulting combined condition. +// +// Return Values: +// Returns true if "cond" combines with the "this" condition. +// "newCond" contains the combines condition. +// +// Operation: +// Check if both conditions are equal. If so, return just 1 of them. +// Reverse their operators and check if their reversed operands match. If so, return either of them. +// +// Notes: +// This is not a full-fledged expression optimizer, it is supposed +// to remove redundant conditions that are generated for optimization +// opportunities. Anything further should be implemented as needed. +// For example, for (i = beg; i < end; i += inc) a[i]. Then, the conditions +// would be: "beg >= 0, end <= a.len, inc > 0" +bool LC_Condition::Combines(const LC_Condition& cond, LC_Condition* newCond) +{ + if (oper == cond.oper && op1 == cond.op1 && op2 == cond.op2) + { + *newCond = *this; + return true; + } + else if ((oper == GT_LT || oper == GT_LE || oper == GT_GT || oper == GT_GE) && + GenTree::ReverseRelop(oper) == cond.oper && op1 == cond.op2 && op2 == cond.op1) + { + *newCond = *this; + return true; + } + return false; +} + +//-------------------------------------------------------------------------------------------------- +// GetLoopOptInfo - Retrieve the loop opt info candidate array. +// +// Arguments: +// loopNum the loop index. +// +// Return Values: +// Return the optInfo array member. The method doesn't allocate memory. +// +ExpandArrayStack<LcOptInfo*>* LoopCloneContext::GetLoopOptInfo(unsigned loopNum) +{ + return optInfo[loopNum]; +} + +//-------------------------------------------------------------------------------------------------- +// CancelLoopOptInfo - Cancel loop cloning optimization for this loop. +// +// Arguments: +// loopNum the loop index. +// +// Return Values: +// None. +// +void LoopCloneContext::CancelLoopOptInfo(unsigned loopNum) +{ + JITDUMP("Cancelling loop cloning for loop L_%02u\n", loopNum); + optInfo[loopNum] = nullptr; + if (conditions[loopNum] != nullptr) + { + conditions[loopNum]->Reset(); + conditions[loopNum] = nullptr; + } +} + +//-------------------------------------------------------------------------------------------------- +// EnsureLoopOptInfo - Retrieve the loop opt info candidate array, if it is not present, allocate +// memory. +// +// Arguments: +// loopNum the loop index. +// +// Return Values: +// The array of optimization candidates for the loop. +// +ExpandArrayStack<LcOptInfo*>* LoopCloneContext::EnsureLoopOptInfo(unsigned loopNum) +{ + if (optInfo[loopNum] == nullptr) + { + optInfo[loopNum] = new (alloc) ExpandArrayStack<LcOptInfo*>(alloc, 4); + } + return optInfo[loopNum]; +} + +//-------------------------------------------------------------------------------------------------- +// EnsureLoopOptInfo - Retrieve the loop cloning conditions candidate array, +// if it is not present, allocate memory. +// +// Arguments: +// loopNum the loop index. +// +// Return Values: +// The array of cloning conditions for the loop. +// +ExpandArrayStack<LC_Condition>* LoopCloneContext::EnsureConditions(unsigned loopNum) +{ + if (conditions[loopNum] == nullptr) + { + conditions[loopNum] = new (alloc) ExpandArrayStack<LC_Condition>(alloc, 4); + } + return conditions[loopNum]; +} + +//-------------------------------------------------------------------------------------------------- +// GetConditions - Get the cloning conditions array for the loop, no allocation. +// +// Arguments: +// loopNum the loop index. +// +// Return Values: +// The array of cloning conditions for the loop. +// +ExpandArrayStack<LC_Condition>* LoopCloneContext::GetConditions(unsigned loopNum) +{ + return conditions[loopNum]; +} + +//-------------------------------------------------------------------------------------------------- +// EnsureDerefs - Ensure an array of dereferences is created if it doesn't exist. +// +// Arguments: +// loopNum the loop index. +// +// Return Values: +// The array of dereferences for the loop. +// +ExpandArrayStack<LC_Array>* LoopCloneContext::EnsureDerefs(unsigned loopNum) +{ + if (derefs[loopNum] == nullptr) + { + derefs[loopNum] = new (alloc) ExpandArrayStack<LC_Array>(alloc, 4); + } + return derefs[loopNum]; +} + +//-------------------------------------------------------------------------------------------------- +// HasBlockConditions - Check if there are block level conditions for the loop. +// +// Arguments: +// loopNum the loop index. +// +// Return Values: +// Return true if there are any block level conditions. +// +bool LoopCloneContext::HasBlockConditions(unsigned loopNum) +{ + ExpandArrayStack<ExpandArrayStack<LC_Condition>*>* levelCond = blockConditions[loopNum]; + if (levelCond == nullptr) + { + return false; + } + + // Walk through each block to check if any of them has conditions. + for (unsigned i = 0; i < levelCond->Size(); ++i) + { + if ((*levelCond)[i]->Size() > 0) + { + return true; + } + } + return false; +} + +//-------------------------------------------------------------------------------------------------- +// GetBlockConditions - Return block level conditions for the loop. +// +// Arguments: +// loopNum the loop index. +// +// Return Values: +// Return block conditions. +// +ExpandArrayStack<ExpandArrayStack<LC_Condition>*>* LoopCloneContext::GetBlockConditions(unsigned loopNum) +{ + assert(HasBlockConditions(loopNum)); + return blockConditions[loopNum]; +} + +//-------------------------------------------------------------------------------------------------- +// EnsureBlockConditions - Allocate block level conditions for the loop if not exists. +// +// Arguments: +// loopNum the loop index. +// condBlocks the number of block-level conditions for each loop, corresponding to the blocks +// created. +// +// Return Values: +// Return block conditions. +// +ExpandArrayStack<ExpandArrayStack<LC_Condition>*>* LoopCloneContext::EnsureBlockConditions(unsigned loopNum, + unsigned condBlocks) +{ + if (blockConditions[loopNum] == nullptr) + { + blockConditions[loopNum] = new (alloc) ExpandArrayStack<ExpandArrayStack<LC_Condition>*>(alloc, condBlocks); + } + ExpandArrayStack<ExpandArrayStack<LC_Condition>*>* levelCond = blockConditions[loopNum]; + for (unsigned i = 0; i < condBlocks; ++i) + { + levelCond->Set(i, new (alloc) ExpandArrayStack<LC_Condition>(alloc)); + } + return levelCond; +} + +#ifdef DEBUG +void LoopCloneContext::PrintBlockConditions(unsigned loopNum) +{ + ExpandArrayStack<ExpandArrayStack<LC_Condition>*>* levelCond = blockConditions[loopNum]; + if (levelCond == nullptr || levelCond->Size() == 0) + { + JITDUMP("No block conditions\n"); + return; + } + + for (unsigned i = 0; i < levelCond->Size(); ++i) + { + JITDUMP("%d = {", i); + for (unsigned j = 0; j < ((*levelCond)[i])->Size(); ++j) + { + if (j != 0) + { + JITDUMP(" & "); + } + (*((*levelCond)[i]))[j].Print(); + } + JITDUMP("}\n"); + } +} +#endif + +//-------------------------------------------------------------------------------------------------- +// EvaluateConditions - Evaluate the loop cloning conditions statically, if it can be evaluated. +// +// Arguments: +// loopNum the loop index. +// pAllTrue all the cloning conditions evaluated to "true" statically. +// pAnyFalse some cloning condition evaluated to "false" statically. +// verbose verbose logging required. +// +// Return Values: +// None. +// +// Operation: +// For example, a condition like "V02 >= V02" statically evaluates to true. Caller should detect such +// conditions and remove them from the "conditions" array. +// +// Similarly, conditions like "V02 > V02" will evaluate to "false". In this case caller has to abort +// loop cloning optimization for the loop. Note that the assumption for conditions is that they will +// all be "AND"ed, so statically we know we will never take the fast path. +// +// Sometimes we simply can't say statically whether "V02 > V01.length" is true or false. +// In that case, the "pAllTrue" will be false because this condition doesn't evaluate to "true" and +// "pAnyFalse" could be false if no other condition statically evaluates to "false". +void LoopCloneContext::EvaluateConditions(unsigned loopNum, bool* pAllTrue, bool* pAnyFalse DEBUGARG(bool verbose)) +{ + bool allTrue = true; + bool anyFalse = false; + + ExpandArrayStack<LC_Condition>& conds = *conditions[loopNum]; + + JITDUMP("Evaluating %d loop cloning conditions for loop %d\n", conds.Size(), loopNum); + + assert(conds.Size() > 0); + for (unsigned i = 0; i < conds.Size(); ++i) + { +#ifdef DEBUG + if (verbose) + { + printf("Considering condition %d: (", i); + conds[i].Print(); + } +#endif + + bool res = false; + // Check if this condition evaluates to true or false. + if (conds[i].Evaluates(&res)) + { + JITDUMP(") evaluates to %d\n", res); + if (!res) + { + anyFalse = true; + return; + } + } + else + { + JITDUMP("), could not be evaluated\n"); + allTrue = false; + } + } + + JITDUMP("Evaluation result allTrue = %d, anyFalse = %d\n", allTrue, anyFalse); + *pAllTrue = allTrue; + *pAnyFalse = anyFalse; +} + +//-------------------------------------------------------------------------------------------------- +// OptimizeConditions - Evaluate the loop cloning conditions statically, if they can be evaluated +// then optimize the "conditions" array accordingly. +// +// Arguments: +// conds The conditions array to optimize. +// +// Return Values: +// None. +// +// Operation: +// For example, a condition like "V02 >= V02" statically evaluates to true. Remove such conditions +// from the "conditions" array. +// +// Similarly, conditions like "V02 > V02" will evaluate to "false". In this case abort loop cloning +// optimization for the loop. +// +// Sometimes, two conditions will combine together to yield a single condition, then remove a +// duplicate condition. +void LoopCloneContext::OptimizeConditions(ExpandArrayStack<LC_Condition>& conds) +{ + for (unsigned i = 0; i < conds.Size(); ++i) + { + // Check if the conditions evaluate. + bool result = false; + if (conds[i].Evaluates(&result)) + { + // If statically known to be true, then remove this condition. + if (result) + { + conds.Remove(i); + --i; + continue; + } + else + { + // Some condition is statically false, then simply indicate + // not to clone this loop. + CancelLoopOptInfo(i); + break; + } + } + + // Check for all other conditions[j], if it would combine with + // conditions[i]. + for (unsigned j = i + 1; j < conds.Size(); ++j) + { + LC_Condition newCond; + if (conds[i].Combines(conds[j], &newCond)) + { + conds.Remove(j); + conds[i] = newCond; + i = -1; + break; + } + } + } +#ifdef DEBUG + // Make sure we didn't miss some combining. + for (unsigned i = 0; i < conds.Size(); ++i) + { + for (unsigned j = 0; j < conds.Size(); ++j) + { + LC_Condition newCond; + if ((i != j) && conds[i].Combines(conds[j], &newCond)) + { + assert(!"Loop cloning conditions can still be optimized further."); + } + } + } +#endif +} + +//-------------------------------------------------------------------------------------------------- +// OptimizeBlockConditions - Optimize block level conditions. +// +// Arguments: +// loopNum the loop index. +// +// Operation: +// Calls OptimizeConditions helper on block level conditions. +// +// Return Values: +// None. +// +void LoopCloneContext::OptimizeBlockConditions(unsigned loopNum DEBUGARG(bool verbose)) +{ + if (!HasBlockConditions(loopNum)) + { + return; + } + ExpandArrayStack<ExpandArrayStack<LC_Condition>*>* levelCond = blockConditions[loopNum]; + for (unsigned i = 0; i < levelCond->Size(); ++i) + { + OptimizeConditions(*((*levelCond)[i])); + } +#ifdef DEBUG + if (verbose) + { + printf("After optimizing block-level cloning conditions\n\t"); + PrintConditions(loopNum); + printf("\n"); + } +#endif +} + +//-------------------------------------------------------------------------------------------------- +// OptimizeConditions - Optimize cloning conditions. +// +// Arguments: +// loopNum the loop index. +// verbose verbose logging required. +// +// Operation: +// Calls OptimizeConditions helper on cloning conditions. +// +// Return Values: +// None. +// +void LoopCloneContext::OptimizeConditions(unsigned loopNum DEBUGARG(bool verbose)) +{ +#ifdef DEBUG + if (verbose) + { + printf("Before optimizing cloning conditions\n\t"); + PrintConditions(loopNum); + printf("\n"); + } +#endif + ExpandArrayStack<LC_Condition>& conds = *conditions[loopNum]; + OptimizeConditions(conds); + +#ifdef DEBUG + if (verbose) + { + printf("After optimizing cloning conditions\n\t"); + PrintConditions(loopNum); + printf("\n"); + } +#endif +} + +#ifdef DEBUG +//-------------------------------------------------------------------------------------------------- +// PrintConditions - Print loop cloning conditions necessary to clone the loop. +// +// Arguments: +// loopNum the loop index. +// +// Return Values: +// None. +// +void LoopCloneContext::PrintConditions(unsigned loopNum) +{ + if (conditions[loopNum] == nullptr) + { + JITDUMP("NO conditions"); + return; + } + if (conditions[loopNum]->Size() == 0) + { + JITDUMP("Conditions were optimized away! Will always take cloned path."); + } + for (unsigned i = 0; i < conditions[loopNum]->Size(); ++i) + { + if (i != 0) + { + JITDUMP(" & "); + } + (*conditions[loopNum])[i].Print(); + } +} +#endif + +//-------------------------------------------------------------------------------------------------- +// CondToStmtInBlock - Convert an array of conditions. Evaluate them into a JTRUE stmt and add it to +// the block +// +// Arguments: +// comp Compiler instance +// conds Array of conditions to evaluate into a JTRUE stmt +// block Block to insert the stmt into +// reverse Reverse conditions if true. +// +// Note: +// The condition that will be generated: jmpTrue(cond1 & cond2 ... == 0) +// +// Return Values: +// None. +// +void LoopCloneContext::CondToStmtInBlock(Compiler* comp, + ExpandArrayStack<LC_Condition>& conds, + BasicBlock* block, + bool reverse) +{ + noway_assert(conds.Size() > 0); + + // Get the first condition. + GenTreePtr cond = conds[0].ToGenTree(comp); + for (unsigned i = 1; i < conds.Size(); ++i) + { + // Append all conditions using AND operator. + cond = comp->gtNewOperNode(GT_AND, TYP_INT, cond, conds[i].ToGenTree(comp)); + } + + // Add "cond == 0" node + cond = comp->gtNewOperNode(reverse ? GT_NE : GT_EQ, TYP_INT, cond, comp->gtNewIconNode(0)); + + // Add jmpTrue "cond == 0" to slow path. + GenTreePtr stmt = comp->fgNewStmtFromTree(comp->gtNewOperNode(GT_JTRUE, TYP_VOID, cond)); + + // Add stmt to the block. + comp->fgInsertStmtAtEnd(block, stmt); + + // Remorph. + comp->fgMorphBlockStmt(block, stmt DEBUGARG("Loop cloning condition")); +} + +//-------------------------------------------------------------------------------------------------- +// Lcl - the current node's local variable. +// +// Arguments: +// None. +// +// Operation: +// If level is 0, then just return the array base. Else return the index variable on dim 'level' +// +// Return Values: +// The local variable in the node's level. +// +unsigned LC_Deref::Lcl() +{ + unsigned lvl = level; + if (lvl == 0) + { + return array.arrIndex->arrLcl; + } + lvl--; + return array.arrIndex->indLcls[lvl]; +} + +//-------------------------------------------------------------------------------------------------- +// HasChildren - Check if there are children to 'this' node. +// +// Arguments: +// None. +// +// Return Values: +// Return true if children are present. +// +bool LC_Deref::HasChildren() +{ + return children != nullptr && children->Size() > 0; +} + +//-------------------------------------------------------------------------------------------------- +// DeriveLevelConditions - Generate conditions for each level of the tree. +// +// Arguments: +// conds An array of conditions for each level i.e., (level x conditions). This array will +// contain the conditions for the tree at the end of the method. +// +// Operation: +// level0 yields only (a != null) condition. All other levels yield two conditions: +// (level < a[...].length && a[...][level] != null) +// +// Return Values: +// None +// +void LC_Deref::DeriveLevelConditions(ExpandArrayStack<ExpandArrayStack<LC_Condition>*>* conds) +{ + if (level == 0) + { + // For level 0, just push (a != null). + (*conds)[level]->Push( + LC_Condition(GT_NE, LC_Expr(LC_Ident(Lcl(), LC_Ident::Var)), LC_Expr(LC_Ident(LC_Ident::Null)))); + } + else + { + // Adjust for level0 having just 1 condition and push condition (i < a.len). + LC_Array arrLen = array; + arrLen.oper = LC_Array::ArrLen; + arrLen.dim = level - 1; + (*conds)[level * 2 - 1]->Push( + LC_Condition(GT_LT, LC_Expr(LC_Ident(Lcl(), LC_Ident::Var)), LC_Expr(LC_Ident(arrLen)))); + + // Push condition (a[i] != null) + LC_Array arrTmp = array; + arrTmp.dim = level; + (*conds)[level * 2]->Push(LC_Condition(GT_NE, LC_Expr(LC_Ident(arrTmp)), LC_Expr(LC_Ident(LC_Ident::Null)))); + } + + // Invoke on the children recursively. + if (HasChildren()) + { + for (unsigned i = 0; i < children->Size(); ++i) + { + (*children)[i]->DeriveLevelConditions(conds); + } + } +} + +//-------------------------------------------------------------------------------------------------- +// EnsureChildren - Create an array of child nodes if nullptr. +// +// Arguments: +// alloc IAllocator instance +// +// Return Values: +// None +// +void LC_Deref::EnsureChildren(IAllocator* alloc) +{ + if (children == nullptr) + { + children = new (alloc) ExpandArrayStack<LC_Deref*>(alloc); + } +} + +//-------------------------------------------------------------------------------------------------- +// Find - Find the node representing the local variable in child nodes of the 'this' node. +// +// Arguments: +// lcl the local to find in the children array +// +// Return Values: +// The child node if found or nullptr. +// +LC_Deref* LC_Deref::Find(unsigned lcl) +{ + return Find(children, lcl); +} + +//-------------------------------------------------------------------------------------------------- +// Find - Find the node representing the local variable in a list of nodes. +// +// Arguments: +// lcl the local to find. +// children the list of nodes to find the node representing the lcl. +// +// Return Values: +// The node if found or nullptr. +// + +// static +LC_Deref* LC_Deref::Find(ExpandArrayStack<LC_Deref*>* children, unsigned lcl) +{ + if (children == nullptr) + { + return nullptr; + } + for (unsigned i = 0; i < children->Size(); ++i) + { + if ((*children)[i]->Lcl() == lcl) + { + return (*children)[i]; + } + } + return nullptr; +} |