Eu tenho uma árvore de decisão binária crítica de desempenho e gostaria de focar esta questão em uma única linha de código. O código para o iterador da árvore binária está abaixo com os resultados da execução da análise de desempenho em relação a ele.
public ScTreeNode GetNodeForState(int rootIndex, float[] inputs)
{
0.2% ScTreeNode node = RootNodes[rootIndex].TreeNode;
24.6% while (node.BranchData != null)
{
0.2% BranchNodeData b = node.BranchData;
0.5% node = b.Child2;
12.8% if (inputs[b.SplitInputIndex] <= b.SplitValue)
0.8% node = b.Child1;
}
0.4% return node;
}
BranchData é um campo, não uma propriedade. Fiz isso para evitar o risco de não ficar inline.
A classe BranchNodeData é a seguinte:
public sealed class BranchNodeData
{
/// <summary>
/// The index of the data item in the input array on which we need to split
/// </summary>
internal int SplitInputIndex = 0;
/// <summary>
/// The value that we should split on
/// </summary>
internal float SplitValue = 0;
/// <summary>
/// The nodes children
/// </summary>
internal ScTreeNode Child1;
internal ScTreeNode Child2;
}
Como você pode ver, a verificação do loop while / null é um grande impacto no desempenho. A árvore é enorme, então eu esperaria que a busca por uma folha demore um pouco, mas eu gostaria de entender a quantidade desproporcional de tempo gasto naquela linha.
Eu tentei:
- Separando a verificação nula do tempo - é a verificação nula que acerta.
- Adicionar um campo booleano ao objeto e compará-lo não fez diferença. Não importa o que está sendo comparado, o problema é a comparação.
Este é um problema de previsão de branch? Em caso afirmativo, o que posso fazer a respeito? Se alguma coisa?
Não vou fingir que entendo o CIL , mas vou postá-lo para qualquer pessoa, para que possam tentar extrair algumas informações dele.
.method public hidebysig
instance class OptimalTreeSearch.ScTreeNode GetNodeForState (
int32 rootIndex,
float32[] inputs
) cil managed
{
// Method begins at RVA 0x2dc8
// Code size 67 (0x43)
.maxstack 2
.locals init (
[0] class OptimalTreeSearch.ScTreeNode node,
[1] class OptimalTreeSearch.BranchNodeData b
)
IL_0000: ldarg.0
IL_0001: ldfld class [mscorlib]System.Collections.Generic.List`1<class OptimalTreeSearch.ScRootNode> OptimalTreeSearch.ScSearchTree::RootNodes
IL_0006: ldarg.1
IL_0007: callvirt instance !0 class [mscorlib]System.Collections.Generic.List`1<class OptimalTreeSearch.ScRootNode>::get_Item(int32)
IL_000c: ldfld class OptimalTreeSearch.ScTreeNode OptimalTreeSearch.ScRootNode::TreeNode
IL_0011: stloc.0
IL_0012: br.s IL_0039
// loop start (head: IL_0039)
IL_0014: ldloc.0
IL_0015: ldfld class OptimalTreeSearch.BranchNodeData OptimalTreeSearch.ScTreeNode::BranchData
IL_001a: stloc.1
IL_001b: ldloc.1
IL_001c: ldfld class OptimalTreeSearch.ScTreeNode OptimalTreeSearch.BranchNodeData::Child2
IL_0021: stloc.0
IL_0022: ldarg.2
IL_0023: ldloc.1
IL_0024: ldfld int32 OptimalTreeSearch.BranchNodeData::SplitInputIndex
IL_0029: ldelem.r4
IL_002a: ldloc.1
IL_002b: ldfld float32 OptimalTreeSearch.BranchNodeData::SplitValue
IL_0030: bgt.un.s IL_0039
IL_0032: ldloc.1
IL_0033: ldfld class OptimalTreeSearch.ScTreeNode OptimalTreeSearch.BranchNodeData::Child1
IL_0038: stloc.0
IL_0039: ldloc.0
IL_003a: ldfld class OptimalTreeSearch.BranchNodeData OptimalTreeSearch.ScTreeNode::BranchData
IL_003f: brtrue.s IL_0014
// end loop
IL_0041: ldloc.0
IL_0042: ret
} // end of method ScSearchTree::GetNodeForState
Edit: Decidi fazer um teste de predição de branch, adicionei um idêntico if dentro do tempo, então temos
while (node.BranchData != null)
e
if (node.BranchData != null)
dentro disso. Em seguida, executei a análise de desempenho em relação a isso e demorou seis vezes mais para executar a primeira comparação do que levou para executar a segunda comparação, que sempre retornou verdadeiro. Portanto, parece que é realmente um problema de previsão de ramificação - e estou supondo que não há nada que eu possa fazer sobre isso ?!
Outra Edição
O resultado acima também ocorreria se node.BranchData tivesse que ser carregado da RAM para a verificação while - ele seria então armazenado em cache para a instrução if.
Esta é minha terceira pergunta sobre um tópico semelhante. Desta vez, estou focando em uma única linha de código. Minhas outras perguntas sobre este assunto são:
while(true) { /* current body */ if(node.BranchData == null) return node; }
. Isso muda alguma coisa?
while(true) { BranchNodeData b = node.BranchData; if(ReferenceEquals(b, null)) return node; node = b.Child2; if (inputs[b.SplitInputIndex] <= b.SplitValue) node = b.Child1; }
Isso seria recuperado node. BranchData
apenas uma vez.
BranchNode
propriedade. Por favor, tente substituirnode.BranchData != null
ReferenceEquals(node.BranchData, null)
. Isso faz alguma diferença?