为什么二进制搜索数组比二叉搜索树略快?

时间:2018-04-09 22:06:06

标签: c algorithm caching optimization data-structures

我使用这两个函数从一大堆数据中搜索查询。它们的速度起初大致相同,但是当尺寸变得非常大时,二进制搜索阵列会稍快一些。那是因为缓存效应吗?数组有顺序。树有吗?

int binary_array_search(int array[], int length, int query){
//the array has been sorted

  int left=0, right=length-1;
  int mid;
  while(left <= right){
    mid = (left+right)/2;
    if(query == array[mid]){
      return 1;
    }
    else if(query < array[mid]){
      right = mid-1;
    }
    else{
      left = mid+1;
    }
  }
  return 0;
}

// Search a binary search tree
int binary_tree_search(bst_t *tree, int ignore, int query){
  node_t *node = tree->root;
  while(node != NULL){
    int data = node->data;
    if(query < data){
      node = node->left;
    }
    else if(query > data){
      node =node->right;
    }
    else{
      return 1;
    }
  }
  return 0;
}

以下是一些结果:

LENGTH   SEARCHES    binary search  array    binary search tree

 1024       10240        7.336000e-03            8.230000e-03
 2048       20480        1.478000e-02           1.727900e-02
 4096       40960        3.001100e-02           3.596800e-02
 8192       81920        6.132700e-02          7.663800e-02
 16384       163840      1.251240e-01          1.637960e-01

2 个答案:

答案 0 :(得分:0)

为什么数组可能并且应该更快有几个原因:

由于leftright指针,树中的节点至少比数组中的项目大3倍。

例如,在32位系统上,您将有12个字节而不是4个字符。这些12个字节被填充或对齐16个字节。在64位系统上,我们得到8和24到32字节。

这意味着使用数组可以在L1缓存中加载3到4倍的项目。

树中的节点在堆上分配,并且它们可以在内存中的任何位置,具体取决于它们的分配顺序(也可以是堆碎片) - 并创建这些节点(使用new或与数组的可能一次性分配相比,alloc}也将花费更多时间 - 但这可能不是速度测试的一部分。

要访问数组中的单个值,只需要进行一次读取,对于我们需要两个的树:leftright指针和值。

当达到较低级别的搜索时,要比较的项目将在数组中靠近(并且可能已经在L1缓存中),而它们可能会在树的内存中传播。

由于locality of reference,大部分时间数组会更快。

答案 1 :(得分:0)

  

这是因为缓存效应吗?

当然,这是主要原因。在现代CPU上,缓存透明地用于在内存中读/写数据。

缓存比主存(DRAM)快得多。为了给你一个观点,访问1级缓存中的数据大约是4个CPU周期,而访问同一CPU上的DRAM大约是200个CPU周期,即快50倍。

缓存在称为缓存行的小块上运行,缓存行通常为64字节长。

更多信息:https://en.wikipedia.org/wiki/CPU_cache

  

数组有顺序。树有吗?

Array是一个单独的数据块。数组的每个元素都与其邻居相邻,即:

+-------------------------------+
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
+-------------------------------+
  block of 32 bytes (8 times 4)

每个数组访问都获取一个缓存行,即64个字节或16个int值。因此,对于数组,存在很高的概率(特别是在二进制搜索结束时),下一次访问将在同一个缓存行中,因此不需要内存访问。

另一方面,树节点是逐个分配的:

                      +------------------------------------------------+
+------------------+  | +------------------+    +------------------+   |
| 0 | left | right | -+ | 2 | left | right | <- | 1 | left | right | <-+
+------------------+    +------------------+    +------------------+
 block 0 of 24 bytes     block 2 of 24 bytes     block 1 of 24 bytes

正如我们所看到的,为了存储3个值,我们使用的内存比在上面的数组中存储8个值多2倍。因此,树结构更稀疏,统计上每个64字节高速缓存行的数据更少。

此外,每个内存分配都会在内存中返回一个块,该块可能与先前分配的树节点不相邻。

此外,分配器将每个内存块对齐至少8个字节(在64位CPU上),因此在那里浪费了一些字节。更不用说我们需要在每个节点中存储leftright个指针......

因此,即使在排序的最后,每个树访问都需要获取一个缓存行,即数组访问速度较慢。

那么为什么数组在测试中的速度稍微快一点呢?这是由于二进制搜索。在排序的最开始,我们非常随机地访问数据,每次访问都与之前的访问相差甚远。因此,数组结构会在排序结束时获得提升。

为了好玩,尝试比较数组中的线性搜索(即基本搜索循环)与树中的二进制搜索。我打赌你会对结果感到惊讶;)