C中使用链表的CPU缓存缺点

C中使用链表的CPU缓存缺点,c,caching,optimization,linked-list,cpu-cache,C,Caching,Optimization,Linked List,Cpu Cache,我想知道与C语言中的连续数组相比,链表的优点和缺点是什么。因此,我读了一篇关于链表的维基百科文章。 根据本文,缺点如下: 由于指针使用的存储空间,它们比数组使用更多的内存 链表中的节点必须从一开始就按顺序读取,因为链表本身就是顺序访问的 当涉及反向遍历时,链表中会出现困难。例如,单链表向后导航很麻烦,而双链表更容易阅读,但在分配内存时会浪费内存 节点是不连续存储的,这大大增加了访问列表中各个元素所需的时间,尤其是使用CPU缓存时 我理解前3点,但我很难理解最后一点: 节点以不连续的方式存储

我想知道与C语言中的连续数组相比,链表的优点和缺点是什么。因此,我读了一篇关于链表的维基百科文章。

根据本文,缺点如下:

  • 由于指针使用的存储空间,它们比数组使用更多的内存
  • 链表中的节点必须从一开始就按顺序读取,因为链表本身就是顺序访问的
  • 当涉及反向遍历时,链表中会出现困难。例如,单链表向后导航很麻烦,而双链表更容易阅读,但在分配内存时会浪费内存

  • 节点是不连续存储的,这大大增加了访问列表中各个元素所需的时间,尤其是使用CPU缓存时

我理解前3点,但我很难理解最后一点:

节点以不连续的方式存储,大大增加了访问列表中各个元素所需的时间,尤其是使用CPU缓存时。

关于CPU缓存的文章没有提到任何关于非连续内存阵列的内容。据我所知,CPU缓存只是缓存经常使用的地址,总共10^-6次缓存未命中


因此,我不明白为什么当涉及到非连续内存阵列时,CPU缓存的效率会降低。

CPU缓存实际上做两件事

您提到的是缓存最近使用的内存

另一个是预测在不久的将来将使用哪种内存。该算法通常非常简单——它假设程序处理大量数据,每当它访问一些内存时,它都会预取后面的几个字节

这不适用于链表,因为节点随机放置在内存中

此外,CPU加载更大的内存块(64128字节)。同样,对于单次读取的int64阵列,它有用于处理8或16个元素的数据。对于链表,它读取一个块,其余的可能会被浪费,因为下一个节点可能位于完全不同的内存块中


最后但并非最不重要的一点是,与上一节相关——链表需要更多的内存用于管理,最简单的版本将至少需要额外的sizeof(指针)字节用于指向下一个节点的指针。但是CPU缓存已经不再那么重要了。

CPU缓存通常接收一定大小的页面,例如(普通页面)4096字节或4kB,并从中访问所需的信息。获取一个页面需要花费相当多的时间,比如说1000个周期。如果我们有一个4096字节的数组,它是连续的,那么我们将从缓存中获取一个4096字节的页面,并且可能大部分数据都在那里。如果没有,我们可能需要获取另一个页面来获取其余的数据

示例:我们有两个0-8191的页面,数组在2048到6244之间,然后我们将从0-4095获取第1页以获得所需的元素,然后从4096-8191获取第2页以获得我们想要的所有数组元素。这将导致从内存中提取2页到缓存以获取数据

但是列表中会发生什么?在列表中,数据是非连续的,这意味着元素不在内存中的连续位置,因此它们可能分散在各个页面中。这意味着CPU必须从内存中提取大量页面到缓存中,才能获得所需的数据

示例:节点1内存地址=1000,节点2内存地址=5000,节点3内存地址=18000。如果CPU能够看到4k大小的页面,那么它必须从内存中提取3个不同的页面来找到它想要的数据

此外,内存使用预取技术在需要之前获取内存页,因此如果链表很小,比如A->B->C,那么第一个周期会很慢,因为预取器无法预测下一个要获取的块。但是,在下一个循环中,我们说预取器已经预热,它可以开始预测链表的路径并及时获取正确的块


摘要数组很容易被硬件预测,并且位于一个位置,因此很容易获取,而链表是不可预测的,并且分散在内存中,这使得预测器和CPU的使用更加困难。

这篇文章只触及了表面,有些地方出错(或者至少有问题),但总体结果通常是一样的:链表的速度要慢得多

需要注意的一点是,“节点是不连续存储的[sic]”是一个过于强烈的说法。的确,在一般情况下,例如,
malloc
返回的节点可能分布在内存中,尤其是在不同时间或从不同线程分配节点时。然而,在实践中,许多节点通常同时分配在同一个线程上,并且这些节点通常在内存中非常连续,因为好的
malloc
实现非常好!此外,当性能是一个问题时,您可能会经常在每个对象的基础上使用特殊的分配器,它从一个或多个连续的内存块中分配固定大小的注释,这将提供很好的空间局部性

因此,您可以假设,至少在某些情况下,链表将为您提供合理的良好空间位置。这在很大程度上取决于您是同时添加大部分列表元素(链表可以),还是在较长时间内不断添加元素(链表的空间位置性较差)

现在,在列表速度缓慢的一面,链表掩盖的一个主要问题是与数组变量相关的一些操作相关的大常量因子。每个人都知道
int find_array(int val, int *array, unsigned int size) {
    for (unsigned int i=0; i < size; i++) {
      if (array[i] == val)
        return i;
    }

    return -1;
}
.L6:
        add     rsi, 4
        cmp     DWORD PTR [rsi-4], edi
        je      .done
        add     eax, 1
        cmp     edx, eax
        jne     .notfound
struct Node {
  struct Node *next;
  int item;
};

Node * find_list(int val, Node *listptr) {
    while (listptr) {
      if (listptr->item == val)
        return listptr;
      listptr = listptr->next;
    }
    return 0;
}
.L20:
        cmp     DWORD PTR [rax+8], edi
        je      .done
        mov     rax, QWORD PTR [rax]
        test    rax, rax
        jne     .notfound
add     rsi, 4
cmp     DWORD PTR [rsi-4], edi