【问题标题】:When does a map become better than two vectors?什么时候地图会比两个向量更好?
【发布时间】:2011-12-14 08:21:23
【问题描述】:

地图对其所有元素进行二进制搜索,这具有对数复杂性 - 这意味着对于足够小的对象集合,地图的性能将比具有线性搜索的两个向量差。

对象(键)池应该有多大才能使地图的性能开始优于两个向量?

编辑:这个问题的一个更通用的版本:对象池应该有多大才能使二进制搜索比线性搜索执行得更好?

我使用字符串作为键,值是指针,但我的特定用例可能无关紧要。我更想了解如何正确使用这两个工具。

【问题讨论】:

  • 你为什么假设你会在向量上执行线性搜索?如果它们是排序的,那么调用binary_searchlower_bound 就可以很简单地进行 O(log N) 搜索。
  • 有足够多的变量,如果这对您很重要,您应该自己安排时间。另请注意,您可以对已排序的向量进行二分搜索。
  • @Matthieu 我猜那么问题就变成了,二分搜索的对象池应该有多大才能比线性搜索执行得更好?
  • 复杂性仅描述渐近行为。任何给定的实现都会有不同的具体运行时间,因为复杂性保证没有说明各种常量的大小。因此,没有比搭建一个简单的测试框架并在您的目标环境中对其进行概要分析更好的解决方案了。
  • 问题不应该是真正的搜索,因为搜索排序的向量(就像地图一样)非常快。问题应该是关于按排序顺序维护向量的成本。

标签: c++ performance map vector


【解决方案1】:

如果你能原谅我这么说的话,在我看来,大多数答案听起来像是在说“我不知道”,而不是真正承认他们不知道。虽然我普遍同意他们给出的建议,但他们似乎都没有尝试直接解决您提出的问题:盈亏平衡点是什么。

公平地说,当我读到这个问题时,我也不知道。这是我们都知道的基础知识之一:对于足够小的集合,线性搜索可能会更快,而对于足够大的集合,二进制搜索无疑会更快。然而,我从来没有真正有太多的理由去调查什么是盈亏平衡点。然而,你的问题让我很好奇,所以我决定写一些代码来了解一下。

这段代码绝对是一个非常快速破解(很多重复,目前只支持一种类型的密钥等),但至少它可以让你知道会发生什么:

#include <set>
#include <vector>
#include <string>
#include <time.h>
#include <iostream>
#include <algorithm>

int main() {   

    static const int reps = 100000;

    std::vector<int> data_vector;
    std::set<int> data_set;
    std::vector<int> search_keys;

    for (int size=10; size<100; size += 10) {
        data_vector.clear();
        data_set.clear();
        search_keys.clear();

        int num_keys = size / 10;   
        for (int i=0; i<size; i++) {
            int key = rand();

            if (i % num_keys == 0)
                search_keys.push_back(key);

            data_set.insert(key);
            data_vector.push_back(key);
        }

        // Search for a few keys that probably aren't present.
        for (int i=0; i<10; i++)
            search_keys.push_back(rand());

        long total_linear =0, total_binary = 0;

        clock_t start_linear = clock();
        for (int k=0; k<reps; k++) {
            for (int j=0; j<search_keys.size(); j++) {
                std::vector<int>::iterator pos = std::find(data_vector.begin(), data_vector.end(), search_keys[j]);
                if (pos != data_vector.end())
                    total_linear += *pos;
            }
        }
        clock_t stop_linear = clock();                      

        clock_t start_binary = clock();
        for (int k=0; k<reps; k++) {
            for (int j=0; j<search_keys.size(); j++) {
                std::set<int>::iterator pos = data_set.find(search_keys[j]);
                if (pos != data_set.end())
                    total_binary += *pos;
            }
        }
        clock_t stop_binary = clock();

        std::cout << "\nignore: " << total_linear << " ignore also: " << total_binary << "\n";

        std::cout << "For size = " << size << "\n";
        std::cout << "\tLinear search time = " << stop_linear - start_linear << "\n";
        std::cout << "\tBinary search time = " << stop_binary - start_binary << "\n";
    }
    return 0;
}

这是我在我的机器上运行的结果:

ignore: 669830816 ignore also: 669830816
For size = 10
        Linear search time = 37
        Binary search time = 45

ignore: 966398112 ignore also: 966398112
For size = 20
        Linear search time = 60
        Binary search time = 47

ignore: 389263520 ignore also: 389263520
For size = 30
        Linear search time = 83
        Binary search time = 63

ignore: -1561901888 ignore also: -1561901888
For size = 40
        Linear search time = 106
        Binary search time = 65

ignore: -1741869184 ignore also: -1741869184
For size = 50
        Linear search time = 127
        Binary search time = 69

ignore: 1130798112 ignore also: 1130798112
For size = 60
        Linear search time = 155
        Binary search time = 75

ignore: -1949669184 ignore also: -1949669184
For size = 70
        Linear search time = 173
        Binary search time = 83

ignore: -1991069184 ignore also: -1991069184
For size = 80
        Linear search time = 195
        Binary search time = 90

ignore: 1750998112 ignore also: 1750998112
For size = 90
        Linear search time = 217
        Binary search time = 79

显然,这不是唯一可能的测试(甚至接近最好的测试),但在我看来,即使是一点硬数据也总比没有好。

编辑:我要郑重说明,我认为使用两个向量(或成对向量)的代码不能像使用集合或映射的代码一样干净。显然你想把它的代码放到一个自己的小类中,但我完全看不出它不能精确地向外界呈现与@987654323相同的界面@ 做。事实上,我可能只是称它为“tiny_map”(或按此顺序排列的东西)。

OO 编程的基本点之一(在泛型编程中至少在某种程度上仍然如此)是将接口与实现分离。在这种情况下,您谈论的纯粹是一个完全不需要影响接口的实现细节。事实上,如果我正在编写一个标准库,我很想将其合并为类似于常见的小字符串优化的“小地图优化”。基本上,只需直接在地图对象本身中分配一个由value_type 的 10 个(左右)对象组成的数组,并在/如果地图很小时使用它们,然后将数据移动到一棵树上,只要它长到足以证明它的合理性.唯一真正的问题是人们是否经常使用小地图来证明这项工作的合理性。

【讨论】:

  • 我将接受的答案更改为您的。令我惊讶的是,从 80 到 90 个对象,二分查找实际上减少了执行时间。那里发生什么了? 0_o
  • @Paul:这有点难以猜测。由于它大约是 10 毫秒,因此很容易或多或少是偶然的(这基本上是一个时间片)。也可能是我得到的确切数字序列最终生成了一个稍微更好的平衡树(RB 和 AVL 树可以防止树变得失去平衡,但仍然可以是 有点比另一个更好)。也有可能只是碰巧将我搜索的键放在靠近根的位置。
  • 你误会了——我不是说“我不知道”,我是说我不能知道。很大的区别。这完全取决于您的编译器、编译器的库、您的机器、您使用它的类型等等。+1 建议隐藏实现。附:对于计时,我在 Windows 中使用 QueryPerformanceCounter,因为它没有粒度问题。
  • @MarkRansom:我的意思是,尽管您无法知道每种可能情况的精确数据,但实际上并不需要花费太多才能获得足够的硬数据做出合理明智的决定。
【解决方案2】:

map 的代码将比两个vectors 的代码干净得多;这应该是您最关心的问题。只有在您确定 map 的性能在您自己的应用程序中存在问题时,您才应该担心差异,此时您应该自己对其进行基准测试。

【讨论】:

  • 如果 map 的性能有问题,我的第一步是要么用 unordered_map 替换它,要么用 boost pool 分配器替换它的分配器。两者(可能)都极大地提高了性能,并且仍然比两个向量简单得多。
【解决方案3】:

地图永远不会等于排序矢量搜索速度,因为矢量内存被更好地打包,并且内存访问更直接。

然而,这只是故事的一半。一旦你开始修改容器,基于节点的容器(不需要移动它们的元素来适应中间的插入),就会获得理论上的优势。与搜索一样,更好的缓存局部性仍然为少量元素的向量提供了优势。

很难提供精确的测量值,并且取决于特定的优化、上下文使用和机器,所以我建议简单地按功能进行。地图具有用于按键搜索的自然界面,因此使用它将使您的生活更轻松。

如果您真的发现自己在测量并且需要真正从用例中挤出性能,那么您可能需要更专业的数据结构。查找 B 树。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-09-27
    • 1970-01-01
    • 1970-01-01
    • 2016-12-10
    • 2012-07-24
    • 1970-01-01
    • 2012-05-29
    • 2020-01-03
    相关资源
    最近更新 更多