我有一个性能关键的二叉决策树,我想把这个问题集中在一行代码上。以下是二叉树迭代器的代码以及对其运行性能分析的结果。
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是一个字段,而不是属性。我这样做是为了防止它不能被内联。
BranchNodeData类如下:
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;
}
正如您所见,while循环/空检查是性能的一个巨大影响。 树是庞大的,所以我希望查找叶子节点需要一段时间,但我想了解在这一行上花费不成比例的时间量。
我尝试过:
- 将Null检查与while分开-Null检查是问题所在。
- 向对象添加布尔字段并针对其进行检查,没有任何区别。 不管比较的是什么,都是比较引起的问题。
这是分支预测问题吗? 如果是,我该怎么办? 如果可以的话?
我不会假装理解CIL,但我会发布它供任何人使用,以便他们可以尝试从中获取一些信息。
.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
编辑:我决定进行一项分支预测测试,我在while循环中添加了一个相同的if语句,因此我们有
while (node.BranchData != null)
和
if (node.BranchData != null)
接着我对其进行了性能分析,第一个比较花费的时间是第二个比较的六倍,而第二个比较总是返回true。因此看起来确实是一个分支预测问题 - 我猜想我无法解决这个问题?!
另一个编辑
如果节点.BranchData必须从RAM加载进行while检查,则会出现上述结果 - 然后将为if语句缓存。
这是我关于类似主题的第三个问题。 这次我重点关注单行代码。 我关于此主题的其他问题:
BranchNode
属性的实现。请尝试将node.BranchData != null
替换为ReferenceEquals(node.BranchData, null)
。这样做有什么区别吗? - Daniel Hilgarthwhile(true) { /* current body */ if(node.BranchData == null) return node; }
。它改变了什么吗? - Daniel Hilgarthwhile(true) { BranchNodeData b = node.BranchData; if(ReferenceEquals(b, null)) return node; node = b.Child2; if (inputs[b.SplitInputIndex] <= b.SplitValue) node = b.Child1; }
这样可以只获取一次node.BranchData
。 - Daniel Hilgarth