【问题标题】:Linked List vs. Array Traversal Efficiency链表与数组遍历效率
【发布时间】:2018-05-26 15:38:39
【问题描述】:

我知道一个数组被分配为一个连续的内存块,因此我们可以通过计算数组开头的字节/字偏移量来访问它的元素。

我知道,由于缓存效率低下,链表遍历效率低于数组遍历,其中分支预测无法像数组那样正常工作。但是,我也听说,由于我们使用偏移量访问数组的方式,从数组的一个元素迭代到下一个元素比访问链表中下一个元素的指针更快。

链表中的指针访问比数组中的偏移访问慢怎么办?

【问题讨论】:

    标签: arrays performance memory data-structures linked-list


    【解决方案1】:

    缓存效率低下,分支预测无法正常工作

    这些是不同的东西。链表存在缓存效率低下的问题:

    1. 节点通常不一定是连续且按顺序分配的,这对空间局部性不利。您有时可以避免这种情况,例如使用自定义分配器。对于分代垃圾回收,在时间中将节点紧密地分配在一起也倾向于将它们在空间中紧密地结合在一起,但这可能在使用链接时实际上并不常见列表。
    2. 在节点中有一个指针(可能还有其他垃圾,如对象头和填充)会浪费空间。浪费一堆空间本质上并不是非常糟糕,但是当浪费的空间被 touch 加载到缓存中时,它就是糟糕的。这实际上发生在这里:指向下一个节点的指针肯定是需要的,另一个垃圾可能在同一个缓存行中,所以它也被拉入。这会浪费缓存空间和带宽(既浪费到更高级别的缓存,也可能浪费到内存),这非常糟糕。

    链接列表本质上并没有真正遭受分支错误预测的影响。是的,如果你遍历一个,最后一个分支(退出循环的那个)很有可能被错误预测,但这并不特定于链表。

    链表中的指针访问比数组中的偏移访问慢怎么办?

    在延迟和吞吐量方面,加载指针比计算数组中元素的下一个地址要慢。为了快速比较,现代机器上的典型情况是加载该点大约需要 4 个周期(充其量!如果有缓存未命中,则需要更长的时间)并且每个周期可以完成两次。将数组元素的大小添加到当前地址需要 1 个周期,每个周期可以完成 4 次,您(或编译器)可以通过一些巧妙的编码重新使用循环计数器的增量。例如,也许您可​​以使用带有循环计数器(无论如何都会递增)作为索引的索引寻址,或者您可以完全“窃取”循环计数器并将其增加一个元素的大小(相应地缩放循环结束),或者没有循环计数器并直接将当前地址与数组末尾之外的地址进行比较。编译器喜欢自动使用这些技巧。

    实际上比听起来要糟糕得多,因为将这些指针加载到链表中是完全串行的。是的,CPU每个周期可以加载两个东西,但是它需要4个周期才能知道下一个节点在哪里,以便它可以开始加载下一个指针,所以实际上它每4个周期只能找到一次节点的地址。计算数组元素的地址没有这样的问题,可能在计算连续地址之间会有 1 的延迟,但是(因为实际的循环无论如何都不能比这更快)只有在展开循环时才会受到伤害,并且如果有必要的话只需添加k*sizeof(element),就可以计算前k步元素的地址(因此可以独立计算多个地址,编译器在展开循环时也会这样做)。

    通过链表在每个“步骤”中做足够多的工作可以隐藏延迟问题。

    【讨论】:

      【解决方案2】:

      访问指针需要额外的内存读取(这比计算慢):要读取下一个元素的值,首先需要从内存中读取指针,然后需要读取引用地址的内容.对于数组,该值只有一次内存读取访问(假设基地址在迭代期间保存在寄存器中)。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2015-10-13
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-09-03
        相关资源
        最近更新 更多