为什么我的应用程序花费其生命的24%做一个空检查?(Why does my application

2019-09-03 07:47发布

我有一个性能关键二元决策的树,我想集中这个问题上的一行代码。 对于二叉树迭代器的代码如下与反对它的运行性能分析的结果。

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

正如你所看到的,而循环/ 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 (node.BranchData != null)

if (node.BranchData != null)

内部的。 然后我跑了针对性能分析,并花了更长的六倍,因为它没有执行总是返回true的第二比较执行第一比较。 所以看起来它确实是一个分支预测的问题 - 我猜有什么我可以做些什么?

另一个编辑

如果node.BranchData不得不从RAM的同时,检查装载也会出现上述结果 - 那么这将被缓存的if语句。


这是我在一个类似的话题第三个问题。 这一次,我专注于一个单一的代码行。 我对这个问题的其他问题有:

  • 我可以用更快的数据结构比这树?
  • 微的优化,通过C#中的树遍历

Answer 1:

树是巨大的

到目前为止,最昂贵的东西处理器永远不会没有执行指令,它是访问内存。 现代的执行核心CPU比内存总线快很多倍。 有关距离的问题,进一步的电信号要旅行,就越难得到得到没有它被破坏传递到电线的另一端该信号。 对于这个问题的唯一的解决办法是让它走慢。 与CPU到内存连接在你的机器电线的一个大问题,可以弹出的情况下, 看到了电线。

处理器有一个应对措施对于这个问题,他们使用高速缓存 ,存储字节的副本RAM缓冲区。 一个重要的一个是L1高速缓存 ,通常为16千字节的数据和16千字节的说明。 体积小,使其接近执行引擎。 读取字节从L1高速缓存通常需要2个或3个CPU周期。 接下来是二级缓存,更大和更慢。 高档的处理器也有L3缓存,更大和更慢呢。 随着工艺技术的改进,这些缓冲区需要更少的空间,因为他们更接近核心,一个很大的原因更新的处理器更好,如何管理使用不断增加的晶体管数自动变快。

这些缓存是不但是一个完美的解决方案。 该处理器将仍然存储器访问搪塞如果数据在缓存中的一个不可用。 它不能继续下去,直到非常缓慢的内存总线已提供的数据。 失去脂肪百个CPU周期有可能在一个单一的指令。

树形结构是一个问题,他们没有缓存友好。 他们的节点往往是分散在整个地址空间。 访问内存的最快方式是通过连续的地址读取。 存储用于L1高速缓存的单元是64个字节。 或者换句话说,一旦处理器读取一个字节,下一个63的速度非常快,因为它们会出现在高速缓存。

这使得目前为止最有效的数据结构的阵列 。 也即在.NET列表<>类不是列表中的所有原因,它使用用于存储的数组。 同样对于其他集合类型,如字典,在结构上不能远程类似的阵列,但阵列内部实现。

所以,你虽然()语句很可能是因为它被取消引用指针访问BranchData场从CPU暂停症。 下面的语句是很便宜的,因为当()语句已经做了从存储器检索值的重任。 分配所述局部变量是便宜的,一个处理器使用用于写的缓冲器。

否则没有一个简单的问题来解决,压扁你的树成阵列很可能是不切实际的。 至少不是因为你通常无法预测,其中责令树的节点将被访问。 红黑树也许会有帮助,它不是来自这个问题清楚了。 因此,一个简单的结论得出的是,它已经在运行速度,你可以期待。 如果你需要它走得更快,那么你就需要更好的硬件以更快的内存总线。 DDR4今年正成为主流。



Answer 2:

为了补充汉斯有关内存缓存很有效果的答案,我的虚拟内存的讨论增加物理内存翻译和NUMA效果。

与虚拟存储器中的计算机(所有当前的计算机),这样做的存储器访问时,每个虚拟存储器地址必须被转换成物理存储器地址。 这是通过使用一个转换表中的内存管理硬件来完成。 该表由操作系统为每个进程管理,并且它本身被存储在RAM中。 对于虚拟内存的每一 ,有在这个转换表映射虚拟到物理页面的条目。 记住汉斯讨论有关内存访问是昂贵的:如果每个虚拟到物理地址的转换需要一个存储器查找,所有的内存访问将花费的两倍多。 解决的办法是具有用于这就是所谓的转换表的高速缓存转换后备缓冲器 (TLB的简称)。 TLB并不大(12〜4096项),并在x86-64架构典型的页面大小只有4 KB,这意味着最多有16 MB与TLB命中直接访问 (它可能比这还要少, 桑迪具有512项的TLB大小桥 )。 数量减少的TLB缺失,可以让操作系统和应用程序协同工作,使用较大的页面大小像2 MB,导致与TLB命中访问更大的内存空间。 此页面介绍了如何使用与Java大页 可以大大加速比存储器访问

如果您的电脑有很多插座,它可能是一个NUMA架构。 NUMA是指非一致内存访问。 在这些架构中, 一些存储器访问的成本比别人多 。 作为一个实例,具有2个插座与计算机32 GB的RAM,每个插座大概有16 GB的RAM。 在此示例计算机,本地存储器访问是价格比访问另一个插座的存储器(远程访问较慢20至100%,甚至更多)。 如果这样的计算机上,您的树使用20 GB的RAM,至少4 GB数据是另一个NUMA节点上,并且如果访问是远程存储器慢50%,NUMA访问减慢你存储器10%的访问。 另外,如果你只有一个NUMA节点上的可用内存,所有的进程需要内存饥饿的节点上会被从该访问是更昂贵的其他节点分配的内存。 即使是最糟糕的,操作系统可以认为这是换出饿死节点的内存, 这将导致更昂贵的内存访问的一部分,是一个好主意。 这在更多的细节解释MySQL的“交换疯狂”的问题和NUMA架构的影响 ,其中一些解决方案,给出了Linux操作系统(扩展内存中的所有NUMA节点上访问,咬远程NUMA子弹访问,以避免交换)。 我也可以认为分配更多的内存插槽(24和8 GB,而不是16和16 GB),并使得确保你的程序是较大的NUMA节点上日程,而这需要计算机和一把螺丝刀物理访问;-) 。



Answer 3:

这本身不是一个答案,而是在什么汉斯帕桑特写在存储系统延迟的重视。

真正的高性能软件 - 比如电脑游戏 - 不仅写来实现游戏本身,它也适于使得代码和数据结构使大部分的高速缓存和内存系统,即把它们当作一种有限的资源。 当我处理缓存问题,我通常假设L1将在3个周期传送如果数据是目前。 如果不是,我得去L2我认为10个周期。 对于L3 30个循环,对RAM存储器100。

有一个附加的内存相关的动作 - 如果你需要使用它 - 施加更大的惩罚,这是一个总线锁定。 如果您使用的Windows NT功能的总线锁被称为关键部分。 如果使用的是自家种的品种,你可以称之为一个自旋锁。 无论它命名为同步下降最慢的总线主控设备的系统之前锁定到位。 最慢的总线主控设备可以是经典的32位PCI卡连接的33MHz @。 33MHz的是一个典型的x86 CPU的频率的百分之一(@ 5.3千兆赫)。 我认为不低于300周期完成总线锁定,但我知道他们可以采取很多次,长,所以如果我看到3000次,我不会感到惊讶。

新手多线程软件开发者会使用总线锁定所有的地方,然后不知道为什么他们的代码是缓慢的。 诀窍 - 与具有与内存做的一切 - 是节约访问。



文章来源: Why does my application spend 24% of its life doing a null check?