【问题标题】:Why binary search array is slightly faster than binary search tree?为什么二叉搜索数组比二叉搜索树略快?
【发布时间】:2018-09-19 09:49:47
【问题描述】:

我使用这两个函数从大量数据中搜索查询。一开始它们的速度差不多,但是当大小变得非常大时,二分搜索数组会稍微快一些。是因为缓存效果吗?数组有顺序。树有吗?

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

【问题讨论】:

  • 很可能是因为分支预测失败。见stackoverflow.com/questions/11227809/…
  • 我认为缓存是更可能的解释。访问数组时,下一个元素已经在缓存中的机会更大。
  • 不知道你是如何得出数组稍微快一点的结论的。即使有一个包含 3200 万个项目的数组/树,也只需要 25 次迭代即可找到答案。我猜你测量的时间与搜索时间几乎没有关系,而与启动成本有很大关系,例如填充数组与构建树。要么是这样,要么你的树不平衡。当树完全平衡时,访问的节点数可能大于log(n) 的理论最小值。事实上,最坏的情况可能是 O(n)。
  • 向我们展示结果,数组的大小?查到了什么项目?搜索顺序?我们不能像这样帮助你。对于不同大小的数组/树,答案可能会有所不同。
  • 好奇:binary_array_search() 执行 if (==) else if (&lt;) elsebinary_tree_search() 执行 if (&lt;) else if (&gt;) else。如果两者都做同样的事情,这将是一个更公平的比较。

标签: c algorithm caching optimization data-structures


【解决方案1】:

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

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

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

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

树中的节点在堆上分配,这些节点可能在内存中的任何地方,这取决于它们被分配的顺序(此外,堆可能会碎片化) - 并创建这些节点(使用 new 或 @987654326 @) 与可能的数组一次性分配相比也将花费更多时间 - 但这可能不是这里速度测试的一部分。

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

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

由于locality of reference,大多数时候数组会更快。

【讨论】:

【解决方案2】:

是因为缓存的影响吗?

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

高速缓存比主存 (DRAM) 快得多。只是为了让您了解一下,访问 1 级缓存中的数据大约需要 4 个 CPU 周期,而访问同一 CPU 上的 DRAM 大约需要 200 个 CPU 周期,即快 50 倍。

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

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

数组有顺序。树有吗?

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

+-------------------------------+
| 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 指针......

所以每个树访问,即使在排序的最后,也需要获取一个缓存行,即比数组访问慢。

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2023-03-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-10-26
    相关资源
    最近更新 更多