Patch: Dynamic hashtable sizing

Steven Grimm sgrimm at facebook.com
Mon Aug 7 20:59:36 UTC 2006


Here's a patch that squeezes another few percent of CPU time improvement 
out of our large caches. It adds auto-resize capability to the memcached 
hash table code.

For small caches, this is a memory win because the initial size is now 
substantially lower, so the constant-factor memory overhead of memcached 
will be less. For large caches, this is a CPU time win because the old 
size (1 million buckets) was pretty easy to exceed substantially if you 
cached a lot of small items. One of our sets of servers has over 40 
million cache entries per server, which means that on average, the old 
code was having to do 40 string compares for each key in a "get" request.

The gotcha: the hashtable has to resize itself periodically. That is not 
really noticeable with cache sizes up to a few hundred thousand entries, 
but beyond that the server will pause while it re-buckets all the 
existing cache entries. The hashtable never shrinks, only grows, so once 
your cache has filled up and you've started expiring old entries (and 
thus the number of entries reaches its maximum) you won't see any more 
pauses. For a 40-million-entry hashtable, you will see a total of 9 
pauses on the way to equilibrium, and only the last couple of them will 
be noticeable on modern hardware.

This patch doubles the size of the hashtable when there are enough 
entries to fill the double-sized table -- that is, when the number of 
entries exceeds 2x the current hashtable size. The performance 
difference between doing it that way and expanding at 1x the current 
size was not really measurable in my tests, and this way eats less 
memory (a 32-million-bucket hashtable on a 64-bit system is not exactly 
tiny.)

-Steve
-------------- next part --------------
Index: assoc.c
===================================================================
--- assoc.c	(revision 19008)
+++ assoc.c	(revision 19009)
@@ -34,8 +34,8 @@
 typedef  unsigned long  int  ub4;   /* unsigned 4-byte quantities */
 typedef  unsigned       char ub1;   /* unsigned 1-byte quantities */
 
-/* hard-code one million buckets, for now (2**20 == 4MB hash) */
-#define HASHPOWER  20
+/* how many powers of 2's worth of buckets we use */
+int hashpower = 16;
 
 #define hashsize(n) ((ub4)1<<(n))
 #define hashmask(n) (hashsize(n)-1)
@@ -127,9 +127,10 @@
 }
 
 static item** hashtable = 0;
+static int hash_items = 0;
 
 void assoc_init(void) {
-    unsigned int hash_size = hashsize(HASHPOWER) * sizeof(void*);
+    unsigned int hash_size = hashsize(hashpower) * sizeof(void*);
     hashtable = malloc(hash_size);
     if (! hashtable) {
         fprintf(stderr, "Failed to init hashtable.\n");
@@ -139,7 +140,7 @@
 }
 
 item *assoc_find(char *key) {
-    ub4 hv = hash(key, strlen(key), 0) & hashmask(HASHPOWER);
+    ub4 hv = hash(key, strlen(key), 0) & hashmask(hashpower);
     item *it = hashtable[hv];
     
     while (it) {
@@ -154,7 +155,7 @@
    the item wasn't found */
 
 static item** _hashitem_before (char *key) {
-    ub4 hv = hash(key, strlen(key), 0) & hashmask(HASHPOWER);
+    ub4 hv = hash(key, strlen(key), 0) & hashmask(hashpower);
     item **pos = &hashtable[hv];
 
     while (*pos && strcmp(key, ITEM_key(*pos))) {
@@ -163,11 +164,38 @@
     return pos;
 }
 
+/* grows the hashtable to the next power of 2, rehashing all the items. */
+static void assoc_expand(void) {
+    item **old_hashtable = hashtable;
+    item **new_hashtable;
+    int i;
+
+    new_hashtable = calloc(hashsize(hashpower + 1), sizeof(void *));
+    if (new_hashtable) {
+        hashpower++;
+        hashtable = new_hashtable;
+        hash_items = 0;
+        for (i = hashsize(hashpower - 1) - 1; i >= 0; i--) {
+            item *it = old_hashtable[i];
+            while (it) {
+                item *next = it->h_next;
+                assoc_insert(ITEM_key(it), it);
+                it = next;
+            }
+        }
+
+        free(old_hashtable);
+    }
+}
+
 /* Note: this isn't an assoc_update.  The key must not already exist to call this */
 int assoc_insert(char *key, item *it) {
-    ub4 hv = hash(key, strlen(key), 0) & hashmask(HASHPOWER);
+    ub4 hv = hash(key, strlen(key), 0) & hashmask(hashpower);
     it->h_next = hashtable[hv];
     hashtable[hv] = it;
+    if (++hash_items > hashsize(hashpower) * 2) {
+        assoc_expand();
+    }
     return 1;
 }
 
@@ -177,6 +205,7 @@
         item *nxt = (*before)->h_next;
         (*before)->h_next = 0;   /* probably pointless, but whatever. */
         *before = nxt;
+        hash_items--;
         return;
     }
     /* Note:  we never actually get here.  the callers don't delete things 


More information about the memcached mailing list