原文:Using the Right Datastructure for the job
译者:杰微刊—刘祥明
键查找 Searching for a key
选择适当的数据结构是一件大家都认同,却很少有人会考虑的事。从我的经验来看, 这不仅是因为它很难引起人们的好奇心,更重要的原因是它需要不厌其烦的进行实验和基准测试,这可能会增加很多不必要的负担。让我们来举个例子。我在日常工作中遇到的很多软件操作都是查找一个key然后对其进行操作,要么检查它是否存在, 要么取出与其相关联的值。为了简单起见, 我们把操作限于检查键是否存在。
基于同样的目的,我们同时将key的类型限制为int。因为int类型是编程语言中最常见、最容易被理解的数据类型。它也是最有可能在各种应用中被用作’id’的类型。
假设我们有一个使用整型id的应用,我们用它来识别客户的身份,我们还打算为它构建一个缓存。缓存是否被命中,决定了我们是否查找数据库。为该需求进行数据结构选择时,hashmap是几乎所有计算机科学学生的首选,它现在在C++中的实现被称为unordered_map。这是由于大O表示法(Big O notation)告诉我们查找哈希表所需要的时间是恒定的。还有哪些备选项?
1. 向量(vector),它的底层实现是一个数组,亦被称为连续的内存(contiguous memory)。
2. 树,它使我们可以进行分类遍历(sorted traversal),但是它也会导致更多的指针和可能的缺页异常(page fault)。
3. 链表(Linked list)。
实际上我们还可以有更多的选项,在这里我就不一一列举了。现在请大家思考一下,既然大O表示法告诉我们使用hashmap已经足够好了, 为什么我们还要考虑其他的选项呢?这是因为有时候大O表示法对我们的应用而言并不能起到很好的指导作用,这时使用小O表示法也许更恰当。我希望读者从中得到的启发是:测量所有东西(Measure Everything)。当需要在自己的机器上进行性能预测时,我们要养成测量的好习惯,因为它是你获得真正答案的唯一方式。 某些人可能会说,“使用向量!那样的话就可以对缓存线(cache lines)进行优化!”事实上,如果我们不把这些不同的方案放到一起进行测量,我们就不可能知道哪个更好。
键生成 Key generation
1
2
3
|
#include#includestd::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> dis( 0, MAX_VAL );
|
以上是我们使用的键提供者,我们用它来生成在某个范围内均匀分布的键。我们尽可能地使用dis,以确保生成再多的key,也能均匀分布在我们指定的范围内。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
###构建索引
//初始化数据结构
std::unordered_map<int,int> myMap;
std::vector<int> myVector;
//生成键
std::cout <<
"Generating keys."
<< std::endl;
for
(size_t i = 0; i != NUM_KEYS; ++i) {
int key = dis(gen);
int val = dis(gen);
if
(myMap.find(key) == myMap.end()) {
myMap[key] = val;
// 添加 key,value 到 map 中
myVector.push_back(key);
}
}
|
我们将同一个key分别放入map和vector(以及任何其它我们想要测量的数据结构)。这为我们提供了一种性能比较和检查代码正确性的方法。不同的数据结构,会在执行相同的查询时,返回相同的结果么?
构建查询条件 Building Queries
1
2
3
4
5
6
|
<pre class=
"brush:js"
>std::cout <<
"Generating Queries."
<< std::endl;
std::vector<int> queries;
queries.resize(NUM_QUERIES);
for
(size_t i = 0; i != NUM_QUERIES; ++i) {
queries[i] = dis(gen);
}</pre>
|
上面的代码很重要。我们事先构建查询条件并将他们存储起来。为此我们需要保证:
1.产生查询条件所需时间不会被纳入真正的查询测量时间中
2.在map和vector上执行相同的查询
3.可以不同的方式来执行查询(sorted, shuffled, 等等)
查询 Querying
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// 查询 map 并记录时间跨度
std::cout <<
"Querying map..."
<< std::endl;
std::chrono::time_point<std::chrono::system_clock> start, end;
start = std::chrono::system_clock::now();
int numMatchesMap = 0;
for
(size_t i = 0; i != NUM_QUERIES; ++i) {
if
(myMap.find(queries[i]) != myMap.end())
++numMatchesMap;
}
end = std::chrono::system_clock::now();
std::chrono::duration<double> elapsed_seconds = end-start;
std::cout <<
"Elapsed time for "
<< NUM_QUERIES <<
" queries in map:"
<< elapsed_seconds.count()
<<
"\nNum matches in map: "
<< numMatchesMap
<<
"\nQuerying vector..."
<< std::endl;
|
上面的代码逻辑很简单,顺序迭代查询条件,然后检查它们是否存在于索引中, 计算时间跨度。最后, 输出结果。测量vector的代码与此相似, 我将不在此贴出,但是我会上传完整的代码。
结果 Results
哎, 我们的计算机科学教授所教我们的是正确的。果真如此吗? 为了对其进行验证,我尝试了不同的参数。简单起见,我将key空间限制在10000以内。
1
2
3
4
|
//常量
int NUM_KEYS = atoi(argv[1]);
int MAX_VAL = atoi(argv[2]);
int NUM_QUERIES = 1000000;
|
Key数量 | Unordered Map查询时间 (s) | Vector 查询时间 (s) |
5 | 0.092408 | 0.048035 |
10 | 0.095659 | 0.067716 |
20 | 0.100039 | 0.130688 |
50 | 0.087938 | 0.254695 |
100 | 0.081867 | 0.479975 |
200 | 0.098481 | 0.888507 |
400 | 0.096329 | 1.73241 |
800 | 0.098282 | 3.29157 |
1600 | 0.097052 | 6.12024 |
3200 | 0.093558 | 10.463 |
6400 | 0.093106 | 16.1336 |
在大多数情况下, unordered map 都优于 vector,有时候甚至比vector超出若干个数量级。但是vector在key数量很小的情况下(<10)却胜出unordered map。 假设这样一个场景:一个应用将查询分发到集群中,集群机器的数量很少但是查询量非常大。在这种场景下,vector应该是正确的选择。但是在大多数其他情况下, 则应该使用hashmap。
组合key Compound Keys
在每个软件工程师的职业生涯中,总会碰到使用组合key的情况。在这种场景中,我们需要将两个key组合成一个单独的key来使用。例如一个应用可能需要在给定领域id的情况下,确定用户是否存在。又如在一个图书馆应用中,我们可能需要使用作者和书名来作为key。这里的键是否有相应的值并不重要。我曾经数次遇到过这种情形,默认情况下,我都选择了对我来说最简单的方案。但是更好的做法是进行测量并理解不同方案之间的利弊,然后做出选择。
数据结构 Datastructures
std::unordered_map。这是我默认使用的方案,创建一个组合key和一些能凑合着用的hasher(makeshift hasher)。然后使用它们来创建unordered_map。
unordered_map<key value="" 2,="" vector >。这是另一个可选的解决方案,将key分成两部分,一部分作为unordered map 的key,另一部分存放在vector中。
我们还可以有其他解决方案:trees,stacks,linear probing maps等,但是我们的目的是演示过程而非答案,所以我只考虑上面这两种方法。在此,我一并展示hashing,key生成,构造查询条件以及查询的相关代码片段。以下代码展示了如何通过组合键来构造map,即构造一个自定义的哈希函数。它们是自包含的,可以编译并运行它们。