應用中如何使用適當的資料結構

發表於2019-05-11
原文: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 = 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) != 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,即構造一個自定義的雜湊函式。它們是自包含的,可以編譯並執行它們。 
完整內容點此檢視
回覆

相關文章