Tengo un árbol de decisiones binarias de rendimiento crítico y me gustaría centrar esta pregunta en una sola línea de código. El código para el iterador de árbol binario se encuentra a continuación con los resultados de la ejecución del análisis de rendimiento en él.
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 es un campo, no una propiedad. Hice esto para evitar el riesgo de que no estuviera alineado.
La clase BranchNodeData es la siguiente:
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 puede ver, la comprobación while / null es un gran éxito en el rendimiento. El árbol es enorme, por lo que esperaría que la búsqueda de una hoja lleve un tiempo, pero me gustaría comprender la cantidad desproporcionada de tiempo que se dedica a esa línea.
He intentado:
- Separando el chequeo nulo del while, el cheque nulo es el acierto.
- Agregar un campo booleano al objeto y compararlo, no hizo ninguna diferencia. No importa lo que se esté comparando, el problema es la comparación.
¿Es este un problema de predicción de rama? Si es así, ¿qué puedo hacer al respecto? ¿Si algo?
No pretendo entender el CIL , pero lo publicaré para que cualquiera lo entienda para que puedan intentar extraer información de él.
.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
Editar: Decidí hacer una prueba de predicción de rama, agregué un si idéntico dentro del tiempo, así que tenemos
while (node.BranchData != null)
y
if (node.BranchData != null)
dentro de eso. Luego ejecuté un análisis de rendimiento en contra de eso, y me tomó seis veces más ejecutar la primera comparación que ejecutar la segunda comparación que siempre resultó verdadera. Así que parece que se trata de un problema de predicción de ramas, ¿y supongo que no hay nada que pueda hacer al respecto?
Otra edición
El resultado anterior también ocurriría si node.BranchData tuviera que cargarse desde la RAM para la verificación while; luego se almacenaría en caché para la declaración if.
Esta es mi tercera pregunta sobre un tema similar. Esta vez me estoy enfocando en una sola línea de código. Mis otras preguntas sobre este tema son:
while(true) { /* current body */ if(node.BranchData == null) return node; }
. ¿Cambia algo?
while(true) { BranchNodeData b = node.BranchData; if(ReferenceEquals(b, null)) return node; node = b.Child2; if (inputs[b.SplitInputIndex] <= b.SplitValue) node = b.Child1; }
Esto se recuperaría node. BranchData
solo una vez.
BranchNode
propiedad. Intente reemplazarnode.BranchData != null
ReferenceEquals(node.BranchData, null)
. ¿Hace alguna diferencia?