【问题标题】:Micro optimisations iterating through a tree in C#在 C# 中迭代​​树的微优化
【发布时间】:2013-05-07 09:51:17
【问题描述】:

我正在从事一个大规模数字运算项目。从一开始我就一直在优化一切,因为我知道这很重要。进行性能分析时,我的代码将其生命周期的近 40% 用于一个函数 - 二叉树迭代器。

        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;
        }

是否有任何 C# 优化专家对进一步优化有任何提示?所有比较都是浮点数。我知道理论上这无关紧要,但我使用的是字段而不是属性,因此请确保优化。这里的小额节省可能会缩短处理时间。

请不要回复说“这些优化在现实世界中无关紧要” - 因为在这种情况下它们确实如此。 :-)

编辑:我已经按照下面的 cmets 将代码更新为现在的代码,并在每行代码的性能分析输出中添加。如您所见,主要杀手是空检查 - 为什么?我尝试在节点上使用布尔标志 IsLeaf 而不是空检查,但它对该行的性能相同。

分支节点对象代码如下:

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;
}

另一个编辑:这里还有更多的思考......我想知道为什么这条线

BranchNodeData b = node.BranchData;

记录了 0.2% 的执行,而空比较行记录了 17.7%。我猜这是分支预测失败?虽然该比较被多次命中,并且几乎总是返回 true,但 CPU 很难预测它何时会返回 false。我不太了解 CPU 的低级工作原理,但这可能是这种情况吗?

【问题讨论】:

  • 一个想法是预取状态(到字典中)并在数据更改时更新它们。这样,当您需要获取节点的状态时,操作将在 O( 1)
  • 只是从 C++ 的角度思考......我不知道 C# 有多少可能,但是当你穿过一棵树时,你已经失去了缓存局部性。如果你所有的对象都分散在内存中,那么局部性就消失了。如果您的树很密集,请考虑将其存储为数组或使用内存池(如果您可以在 C# 中执行此操作)。您也将成为此处分支预测的受害者。不确定您是否可以为此做很多事情。
  • 一个次要的事情可能是将node.BranchData存储在一个临时变量中,而不是在每次while循环迭代中加载该字段三次。
  • 尝试用node = node.BranchData.Children[(1 + Math.Sign(inputs[node.BranchData.SplitInputIndex] - node.BranchData.SplitValue))/2];替换if
  • @WillCalderwood 因为这是一棵二叉树(我认为),是否有可能放弃 Children 集合而直接引用 Child1Child2 并避免集合查找?

标签: c# optimization binary-tree


【解决方案1】:

只是一些代码重写。这可能会有所帮助,因为它至少避免了两次跳跃。

public ScTreeNode GetNodeForState(int rootIndex, float[] inputs)
{

    ScTreeNode node = RootNodes[rootIndex].TreeNode;

    while (node.BranchData != null)
    {
        BranchNodeData b = node.BranchData;
        node = b.Child2;
        if (inputs[b.SplitInputIndex] <= b.SplitValue))
            node = b.Child1;
    }

    return node;

}

【讨论】:

  • 谢谢。我进行了最新的性能测试。搜索 1GB 树中的每一片叶子 100 次。我的代码用了 5190 毫秒,你的用了 4827 毫秒。虽小,但有进步。 :-)
【解决方案2】:

BranchNodeData 看起来像一个引用类型。它只占运行时的 0.2%,因为它只是创建一个指向已经存在的数据的指针,而不是实际复制或分配任何东西。

您可能在 null 检查中受到了打击,因为 CLR 必须进行强制转换才能检查您粘贴的密封类。检查 null 不一定是您所追求的.有很多方法可以修改该类,为您提供一个布尔值来检查它不需要太多的计算能力。老实说,我会选择你的 ScTreeNode 类可以提供的东西。

【讨论】:

  • 我猜你没有读到这一点“我尝试在节点上使用布尔标志 IsLeaf 而不是空检查,但它对该行的性能相同。” ;-)
【解决方案3】:

鉴于其他答案中关于缓存的观点,但与空检查无关,请尝试对BranchNodeData 字段的引用进行排序,以便第一个引用允许将以下所有字段加载到缓存中.

也就是说,我假设在当前代码中首先引用 Child2 时,Jitter 或 CPU 不够智能,无法“向后”加载以缓存 SplitInputIndexSplitValueChild1

因此要么更改BranchNodeData 类中字段的顺序,要么将set; if ... overwrite; 更改为if ... else

【讨论】:

    猜你喜欢
    • 2017-07-19
    • 1970-01-01
    • 2015-05-08
    • 2010-11-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-10-22
    • 2020-07-14
    相关资源
    最近更新 更多