MapEmbed: Perfect Hashing with High Load Factor and Fast Update
Yuhan Wu∗ Zirui Liu∗ Xiang Yu∗ Peking University Peking University Peking University
Jie Gui∗ Haochen Gan† Yuhao Han‡ Peking University Peking University National University of Defense Technology
Tao Li‡ Ori Rottenstreich§ Tong Yang∗¶ National University of Defense Technion Peking University Technology
ABSTRACT ACM Reference Format: 1 Perfect hashing is a hash function that maps a set of distinct Yuhan Wu, Zirui Liu, Xiang Yu, Jie Gui, Haochen Gan, Yuhao Han, Tao keys to a set of continuous integers without collision. However, Li, Ori Rottenstreich, and Tong Yang. 2021. MapEmbed: Perfect Hashing with High Load Factor and Fast Update. In Proceedings of the 27th ACM most existing perfect hash schemes are static, which means that SIGKDD Conference on Knowledge Discovery and Data Mining (KDD ’21), they cannot support incremental updates, while most datasets in August 14–18, 2021, Virtual Event, Singapore. ACM, New York, NY, USA, practice are dynamic. To address this issue, we propose a novel 10 pages. https://doi.org/10.1145/3447548.3467240 hashing scheme, namely MapEmbed Hashing. Inspired by divide- and-conquer and map-and-reduce, our key idea is named map- and-embed and includes two phases: 1) Map all keys into many 1 INTRODUCTION small virtual tables; 2) Embed all small tables into a large table by 1.1 Background and Motivation circular move. Our experimental results show that under the same Hashing is one of the most fundamental techniques and has a experimental setting, the state-of-the-art perfect hashing (dynamic wide range of applications in various fields, such as data mining, . perfect hashing) can achieve around 15% load factor, around 0 3 databases, machine learning, storage, networking [1–6]. Perfect ∼ Mops update speed, while our MapEmbed achieves around 90% hashing is a special hash function that maps a set of different keys . 95% load factor, and around 8 0 Mops update speed per thread. All to a set of continuous integers without collision. It often consists of codes of ours and other algorithms are open-sourced at GitHub. two parts: a small index and a large Key-Value (KV) table. The index is often small enough to be held in fast memory (e.g., CPU/GPU CCS CONCEPTS Caches, SRAM in FPGA/ASIC), while the KV table is typically larger • Information systems → Data structures. and is stored in slow memory (e.g., DDR [7], DRAM [8]). For each insertion/lookup, perfect hashing first updates/looks-up the index KEYWORDS then the KV table. Perfect Hashing has two advantages: 1) its lookup Perfect Hashing, Hash Tables, KV Stores time is always constant because hash collision is eliminated, and each lookup needs only one hash probe in slow memory; 2) its ∗Department of Computer Science and Technology, Peking University, China index is often very small (e.g., 1.56 bits per key [9]), and can be †School of Mathematical Sciences, Peking University, China ignored compared with the large KV table (e.g., each KV pair needs ‡School of Computer, National University of Defense Technology, Changsha, China hundreds of Bytes or more); Thanks to these two advantages, the § The Department of Computer Science and the Department of Electrical Engineering, perfect hash is widely used in many fields, such as databases [10], Technion – Israel Institute of Technology, Haifa, Israel ¶National Engineering Laboratory for Big Data Analysis Technology and Application, networks [11], and bioinformatics [12]. Peking University, China Most existing perfect hash schemes are static [13–17], which 1Yuhan Wu, Zirui Liu, and Xiang Yu contribute equally to this paper. Tong Yang means that they cannot support incremental updates, while most ([email protected]) is the corresponding author. datasets in practice are dynamic. In order to insert new keys, the perfect hash can only reconstruct the whole function from the be- Permission to make digital or hard copies of all or part of this work for personal or classroom use is granted without fee provided that copies are not made or distributed ginning. Among all existing works, only the Dynamic Perfect Hash for profit or commercial advantage and that copies bear this notice and the full citation (DPH) [18] supports incremental updates, drastically extending the on the first page. Copyrights for components of this work owned by others than ACM application range of perfect hashing. Its key idea is using the divide- must be honored. Abstracting with credit is permitted. To copy otherwise, or republish, to post on servers or to redistribute to lists, requires prior specific permission and/or a and-conquer methodology: it first divides the datasets into many fee. Request permissions from [email protected]. small ones; and for each small dataset, it builds a perfect hash table KDD ’21, August 14–18, 2021, Virtual Event, Singapore. using existing hash schemes. Therefore, each incremental update © 2021 Association for Computing Machinery. ACM ISBN 978-1-4503-8332-5/21/08...$15.00 is indeed to reconstruct a small perfect hash table. However, to https://doi.org/10.1145/3447548.3467240 control the reconstruction time, it needs to use many hash buckets for each small perfect hash table. Therefore, its load factor2 can only We will try embedding after one circular move – pushing all KV achieve 10% ∼ 20% (see Figure 11(a)), i.e., it is memory inefficient. pairs in each virtual bucket into its associated real bucket. If the The design goal of this paper is to keep all advantages of existing embedding succeeds, the insertion is complete. Otherwise, repeat works (constant lookup time, and small index structure) and, at the the circular movement and embedding. The number of circular same time, to support fast incremental updates and achieve a high moves (the offset number of the keys in the same virtual table) load factor. is recorded in the corresponding index cell. The source codes of MapEmbed and other related algorithms are available at GitHub 4. 1.2 Our Proposed Solution Main Experimental Results: 1) MapEmbed’s index structure is Towards the design goal, we propose a novel perfect hashing scheme, quite small (e.g., 0.5 ∼ 1.6 bits per key); 2) MapEmbed supports fast namely MapEmbed Hashing. It has the following advantages: 1) incremental updates (20× faster than Dynamic Perfect Hashing); 3) it has no hash collision; 2) its lookup time is always constant; 3) its MapEmbed’s load factor is high (90% ∼ 95%); 4) We implemented index is quite small (e.g., 0.5 ∼ 1.6 bits per key); 4) it supports fast MapEmbed on a FPGA platform, achieving fast lookup speed (367 incremental updates (20× faster than Dynamic Perfect Hashing); 5) Million operations per second). its load factor is high (90% ∼ 95%). 2 RELATED WORK KV pairs Existing hashing schemes can be divided into two categories: per- fect hashing without collision and imperfect hashing with colli- Map sion. Perfect hashing can be further divided into two categories: Index Table ����� static perfect hashing and dynamic perfect hashing. Perfect hash- ing schemes only need one probe of a bucket for each query, but Cells 0 0 0 0 0 0 0 0 1 0 1 ���� cannot support incremental updates. In practice, one bucket can � store multiple KV pairs. Thanks to cache line and SIMD (Single
Virtual Virtual update Instruction and Multiple Data [19]), probing a bucket with multiple Table Buckets to KV pairs achieves similar speed with probing a bucket with only Fast Memory Circular Move one KV pair. Therefore, in our perfect hashing, we store multiple KV Embed pairs in each bucket to fully utilize the characteristics of commodity KV Table memory. For more hashing schemes, please refer to the literature Real [20–24], and the survey [25]. Buckets Slow Memory 2.1 Perfect Hashing Figure 1: An example of MapEmbed Hashing. Perfect hashing searches hash functions that map n distinct ele- ≥ Inspired by divide-and-conquer and map-and-reduce, our key ments in a set S to m (m n) buckets without collision. When idea is named map-and-embed, including two phases. 1) Map: m = n, it is called minimal perfect hashing. map all keys into many small virtual tables. 2) Embed: merge and Static Perfect Hashing expects to find a perfect hash function for embed all virtual tables into a large table by circular moving. each key in a given set. Typical schemes of static perfect hashing We use an example to present the two phases. As shown in Figure include FKS, CHD, BDZ, BMZ, BRZ, CHM, and FCH [13–17, 26–29]. 1, the data structure of MapEmbed Hashing consists of two parts: The FKS hashing [14] consists of many buckets, and each bucket an index table in fast memory and a large KV table in slow memory. has many slots. To insert an item, FKS first hashes it into a subtable The index table is an array of index cells, each cell corresponds to a and uses another hash function to hash it into a bucket in that small virtual table consisting of virtual buckets, and each virtual subtable. When a hash collision occurs in a bucket, the FKS hashing bucket can store multiple KV pairs. First, we map each key into a changes the hash function of the subtable repeatedly until there 3 is no collision. To reduce hash collision, the load factor of each virtual bucket of an index cell using two hash functions: Hash1(·) √ subtable is kept very low, i.e., below 1/ k where k is the size of and Hash2(·). Second, we embed each virtual table into the KV table, which consists of real buckets. Specifically, given a virtual each subtable. table, each virtual bucket is associated with a real bucket in the Dynamic Perfect Hashing (DPH) [18] aims to support incre- KV table, and we push all KV pairs in each virtual bucket into its mental update based on FKS [14]. Its key idea is divide-and-conquer: associated real bucket. If no bucket in the large table overflows, the it divides the KV pairs into many groups and builds one small per- embed phase succeeds. Otherwise, we circularly move the keys in fect hash table for each group. When a hash collision happens in a the virtual table and try to embed them again. Specifically, for all small perfect hash table, it just reconstructs the table by changing keys in the virtual table, we will move them to the next adjacent the hash function by brute force. To make reconstructions infre- virtual bucket. In other words, in one circular move, we will move quent, the load factor of each subtable is kept very low (often less the key in the i-th virtual bucket to the (i + 1)-th virtual bucket and than 15%). It is the most well-known work to enable Perfect hashing move the key in the last virtual bucket to the first virtual bucket. to support increment update, but it is inefficient in both update speed and memory usage. Compared to dynamic perfect hashing, 2Load factor is the ratio of the number of inserted keys to the number of slots, where each slot can store one key. 3“Virtual bucket” is only a logical concept, not a bucket that actually stores KV pairs. 4https://github.com/MapEmbed/MapEmbed we aim to improve both the load factor and update speed signifi- cells and is associated with a hash function Hc (·). Each cell j stores cantly. an offset number C[j]. Each cell j is associated with D buckets by ( ) ( ) ( − ) bucket functions 0 ( ), 1 ( ),..., D 1 ( ) hb j hb j hb j , which are called 2.2 Imperfect Hashing the D associated buckets of cell j. Specifically, the D asso- ( ) ( ) ( − ) Most existing hash schemes [30–35] are imperfect hashings, which B[ 0 ( )], B[ 1 ( )],..., B[ D 1 ( )] ciated buckets of cell j are hb j hb j hb j . cannot achieve one hash probe per lookup. In these schemes, hash The main symbols used in this paper are shown in Table1. collision is inevitable and addressed using different strategies. Typi- cal solutions include hash chaining, open addressing, using multiple subtables, etc. 3 9 7 2 Cuckoo hashing [30] is an illuminating hashing scheme using j = ℋ3 456 = 4 the kick operation. It has two hash functions corresponding to two 116 . = 9 subtables, and each table consists of multiple buckets. To insert a (" 0 ~" 8 ) key, it selects two candidate buckets from the two tables by hashing i = (ℋ= 456 + " ; )%' = 1 and checks if there is an available (not full) bucket among the two buckets. If so, it inserts the key into the available bucket, and the 11 6 664 7 8 0 7 3 9 7 ' = 3 8 0 76 6745 , = 2 7 26 insertion succeeds. Otherwise, it kicks an old key keyold from one of the two candidate buckets and inserts the new key into it. Then ℎ(:) ; = 3 it probes the other candidate bucket of keyold and tries to insert 9 the old key to the bucket. If the bucket is unavailable, the kick operation performs again. This procedure repeats until there is 8 0 76 no collision. When allowing at most 500 kicks and each bucket (ℬ 0 ~ℬ 9 ) / = 10 stores four items, the cuckoo hashing can achieve a load factor of 95%. Cuckoo hashing needs two hash probes in the worst case for Figure 2: Insertion Operation of MapEmbed. each lookup, and 1.5 on average. Many variants and applications of cuckoo hashing have achieved great success, including BCHT [31], cuckoo filter [32, 36], and more [33–35, 37–43]. 3.2 Basic Operations Insertion: The process to insert a new item ⟨key,value⟩ is illus- Table 1: Main Symbols Used in This Paper. trated in Figure2 and Figure3. There are four steps. The Map Phase includes the first two steps, and the Embed phase includes the rest Symbol Meaning two steps. The details are as follows: S Number of cells in fast memory • H ( ) M Number of buckets in slow memory Step 1: select a cell. We select cell j by calculating j = c key . • Step 2: select a bucket. The bucket is selected from the D as- D Number of buckets that a cell is associated with ( ) ( ) B[ 0 ( )], B[ 1 ( )],..., N Number of items a bucket can store sociated buckets of cell j, i.e., from hb j hb j ( − ) L Number of layers of cell arrays in fast memory B[ D 1 ( )] = (H ( )+ hb j . First, we calculate an index valuei byi D key C[j] The offset number stored in the jth cell (cell j) C[j]) mod D, where C[j] is the offset number stored in cell j. th Hc (·) The hash function mapping a key to a cell Second, we select the i bucket to place the item, i.e., select ( ) The hash function mapping a key to an index, B[h i (j)]. HD (·) b which is an integer in [0, D − 1] ( ) • Step 3: insert the item. We insert the item into bucket B[h i (j)]. bucket function b The hash function ( ) that is used (i) (i) th B[ ( )] (·) for selecting the i (i ∈ [0, D − 1]) associated Note this may cause hb j overflow. If overflow does not hb bucket given a cell happen, the insertion completes. Otherwise, we go to step 4. • Step 4: update offset and move items. We first check whether i The hash function that is used for constructing h (·) (i) C[j] = D − 1. If so, which means the items of cell j cannot be h (·) b moved, the insertion fails. Otherwise, we increment C[j] by one. The ith (i ∈ [0, D − 1]) bucket that is associated B[ (i)( )] Then we perform a circular move operation as follows. For hb j with cell j each of the D associated buckets of cell j, for each item in it, we ex- ecute step 1 to check whether the item is mapped into cell j. Take 3 THE MAPEMBED HASH TABLE th B[ (i)( )] the i associated bucket hb j as an example, for each item ⟨key0,value0⟩ in it, we check whether Hc (key0) = j. If so, we 3.1 Data Structure (( ) ) move the item to the next associated bucket B[h i+1 mod D (j)]. As shown in Figure2, the data structure of MapEmbed consists of b two parts: 1) A large KV table used to store KV pairs (items). This Finally, we check if there is any bucket overflow under the current table is stored in slow memory. It is organized as an array of M buck- arrangement. If not, the insertion operation completes. Other- ets, B[0], B[1],..., B[M − 1], where each bucket can store N items. wise, we repeat step 4. 2) A small index table used to track the index of the current items. Lookup: The process to look up a key is illustrated in Figure4 This table is stored in fast memory. It is organized as an array of m and proceeds as follows: Insert ��� Example 3: The red item ⟨key3,value3⟩ is mapped into cell 3 and Step 1 Select cell � by calculating bucket B[4], and overflow happens at B[4]. We increment C[3] by Select a Cell � = ℋ!(���) one and perform the circular move operation for cell 3.
# Analysis: Due to space limitation, we present the theoretical anal- Step 2 Select bucket ℬ[ℎ" (�)], Select a Bucket where � = ℋ$ ��� + � � %� Map ysis about how the parameter of MapEmbed influences its perfor- mance in AppendixA. Step 3 # Insert ���, ����� into ℬ[ℎ" (�)] Check the Bucket 3.3 Dynamic Expansion ℬ[ℎ # (�)] No " End In practical applications, the dataset often changes dynamically, overflow? so we cannot always set the optimal size of the KV table before- Yes Yes End hand. To address this problem, we propose a dynamic expansion � � = � − 1? (Fail) algorithm, which doubles the number of buckets in slow memory, No Step 4 while keeping the cells in fast memory unchanged. � � += 1; Update Offset Circular Move Operation As mentioned before, the D associated buckets of cell j are ( ) ( ) ( − ) & Move Items bucket functions 0 ( ), 1 ( ),..., D 1 ( ) selected by hb j hb j hb j . We Yes Any bucket first elaborate on how the D bucket functions are constructed. overflow? This is done by D hash functions h0(·), h1(·),..., hD−1(·) : cell 7→ No Embed {0, ..., INT _MAX }, which hash a cell (or a cell’s unique ID) to End ( ) an integer. The ith bucket function is constructed by h i (·) = Figure 3: Insertion Operation. b hi (·) mod M, where M is the number of buckets in slow memory. Our dynamic expansion algorithm proceeds as follows. First, Lookup ��� we perform a copy operation to copy all the M buckets in slow Step 1 Select cell � by calculating memory, and then append the copied M buckets to the original M � = ℋ (���) Select a Cell ! buckets. After this, there are 2M buckets (numbered 0, 1,..., 2M−1) # in slow memory, where B[0] is the same as B[M], B[1] is the same Step 2 Select bucket ℬ[ℎ" (�)], Select a Bucket where � = ℋ$ ��� + � � %� as B[M + 1], etc. Second, we mark all buckets in slow memory as redundant, meaning there could be redundant items in the bucket. Yes Step 3 ��� in ℬ[ℎ # (�)]? End Afterward, when a redundant bucket is probed during inser- Check the Bucket " No tion/lookup operation, we perform a redundancy-clean opera- End tion: For each item in this bucket, we check whether it is a redun- Figure 4: Lookup Operation. dancy. Specifically, given a redundant bucket B[b], for each item ( ) ⟨ , ⟩ i ( ) = H ( ) key value in it, we check whether b , hb j , where j c key , • Step 1: select a cell. We select cell j for the key by calculating and i = (HD (key) + C[j]) mod D. If so, which means the item is a j = Hc (key). redundancy, we delete it from bucket b. After checking all items in ( ) • B[ i ( )] = a redundant bucket, we mark the bucket as “clean”. Step 2: select a bucket. We select bucket hb j where i (HD (key) + C[j]) mod D. Analysis: We briefly explain why the expansion operation is rea- • B[ (i)( )] sonable. For an item ⟨key,value⟩, suppose before expansion, it is Step 3: check the selected bucket. We search bucket h j ( ) b B[ ] = H ( ) = i ( ) = for key and return the results. mapped to cell j and bucket b , where j c key , b hb j hi (j) mod M, and i = (H (key) + C[j]) mod D. After expansion, Deletion: To delete an item, we first look up it and then delete it. D the item exists in both B[b] and B[b + M]. According to our rules Examples: We illustrate the circular move operation in Figure5. for selecting the bucket in step 1, the item should be placed in B[b′], The three examples show a continuous insertion process of three ( ) ′ = i ( ) = ( i ( ) ) different KV pairs. Each column is an example. where b hb j h j mod 2M . And we have: ⟨ ⟩ Example 1: The blue item key1,value1 is mapped to cell 5 by (hi (j) mod M) = b th (hi (j) mod 2M) = Hc (key1) = 5 and is dispatched to the 3 associated bucket (B[4]) ( i ( ) ) + = + ( ) h j mod M M b M of cell 5, where H (key ) = 3 and h i (j) = 4. As bucket B[4] is full, D 1 b i i we increment C[5] by one and start the circular move operation. For example, suppose h (j) = 10 and M = 7, then b = h (j) mod ′ i For each of the D associated buckets of cell 5 (the buckets with M = 10 mod 7 = 3. And b = h (j) mod (2M) = 10 mod 14 = 3 + 7 white background), we check each item in it for whether the item = b + M. This property guarantees that each item can still be is mapped into cell 5 in step 1. Then we move all the items mapped retrieved after the expansion operation. to cell 5 (the colored items) to the next associated bucket. After this operation, no bucket overflows, so the insertion completes. 3.4 Optimization using Multi-Layer Index Example 2: The green item ⟨key2,value2⟩ happens to be mapped To improve the load factor, we propose the multi-layer version of to cell 5 again. Next, it is dispatched to a full bucket B[7]. Then we MapEmbed by dividing the cell array into L (e.g., 3) layers (see increment C[5] and perform the circular move operation twice to Figure6). Each layer has an independent hash function Hc (·) to arrange all items mapped into cell 5 successfully. map a key into a cell. The hash function of the ith layer is denoted Index Table Index Table Index Table New Item New Item New Item ��� , ����� ��� , ����� ��� , ����� �� ���� = � �� ���� = � �� ���� = � Cell Array Cell Array Cell Array 0 0 1 0 0 0 0 0 1 0 0 0 1 0 0 1 0 0 1 0 0 0 1 0 0 3 0 0 1 0 � 0 ~� 10 1 � 0 ~� 10 3 � 0 ~� 10 1 Fast Memory Fast Memory Fast Memory
KV Table � = �� ���� + � � ��� � KV Table � = �� ���� + � � ��� � KV Table � = �� ���� + � � ��� �
Bucket Bucket Bucket Array Array Array 0 1 2 3 4 5 6 7 8 9 10 0 1 2 3 4 5 6 7 8 9 10 0 1 2 3 4 5 6 7 8 9 10 Circular Move Circular Move Circular Move
Bucket Bucket Bucket Array Array Array 0 1 2 3 4 5 6 7 8 9 10 0 1 2 3 4 5 6 7 8 9 10 0 1 2 3 4 5 6 7 8 9 10 Slow Memory Slow Memory Slow Memory
Tetris Illustration Tetris Illustration Tetris Illustration
× Move × Move Move max max × max (1 grid) (2 grids) (1 grid)
Example 1 Example 2 Example 3 Figure 5: Three examples of MapEmbed illustrating the circular move operation (S = 11, D = 5, N = 4, and M = 11).