1 #ifndef SIMPLIFIED_CLIFFC_HASHTABLE_H
2 #define SIMPLIFIED_CLIFFC_HASHTABLE_H
14 This header file declares and defines a simplified version of Cliff Click's
15 NonblockingHashMap. It contains all the necessary structrues and main
16 functions. In simplified_cliffc_hashtable.cc file, it has the definition for
20 template<typename TypeK, typename TypeV>
21 class cliffc_hashtable;
24 Corresponding the the Object[] array in Cliff Click's Java implementation.
25 It keeps the first two slots for CHM (Hashtable control unit) and the hash
26 records (an array of hash used for fast negative key-equality check).
34 int real_size = sizeof(atomic<void*>) * 2 + 2;
35 _data = new atomic<void*>[real_size];
36 // The control block should be initialized in resize()
37 // Init the hash record array
38 int *hashes = new int[_size];
40 for (i = 0; i < _size; i++) {
43 _data[1].store(hashes, memory_order_relaxed);
44 // Init the data to Null slot
45 for (i = 2; i < real_size; i++) {
46 _data[i].store(NULL, memory_order_relaxed);
51 int *hashes = (int*) _data[1].load(memory_order_relaxed);
59 shared_ptr<void> _ptr;
61 slot(bool prime, shared_ptr<void> ptr) {
70 TypeK must have defined function "int hashCode()" which return the hash
71 code for the its object, and "int equals(TypeK anotherKey)" which is
72 used to judge equality.
73 TypeK and TypeV should define their own copy constructor.
74 To make the memory management safe and similar to Cliff Click's Java
75 implementation, we use shared_ptr instead of normal pointer in terms of the
76 pointers that point to TypeK and TypeV.
78 template<typename TypeK, typename TypeV>
79 class cliffc_hashtable {
81 # The synchronization we have for the hashtable gives us the property of
82 # serializability, so we should have a sequential hashtable when we check the
83 # correctness. The key thing is to identify all the commit point.
88 spec_hashtable<TypeK, TypeV*> __map;
89 spec_hashtable<TypeK, Tag> __id_map;
92 static bool _val_equals(TypeV *ptr1, TypeV *ptr2) {
96 # Update the tag for the current key slot if the corresponding tag
97 # is NULL, otherwise just return that tag. It will update the next
98 # available tag too if it requires a new tag for that key slot.
99 static Tag _getKeyTag(TypeK &key) {
100 if (__id_map.get(key) == NULL) {
101 Tag cur_tag = tag.current();
102 __id_map.put(key, cur_tag);
106 return __id_map.get(key);
111 Read_interface = Get + PutIfAbsent + RemoveAny + RemoveIfMatch +
112 ReplaceAny + ReplaceIfMatch;
113 Write_interface = Put + PutIfAbsent(__RET__ == NULL) + RemoveAny +
114 RemoveIfMatch(__RET__ == true) + ReplaceAny +
115 ReplaceIfMatch(__RET__ == true);
117 Write_interface -> Read_interface
123 The control structure for the hashtable
127 friend class cliffc_hashtable;
129 atomic<kvs_data*> _newkvs;
131 // Size of active K,V pairs
134 // Count of used slots
137 // The next part of the table to copy
138 atomic_int _copy_idx;
140 // Work-done reporting
141 atomic_int _copy_done;
145 _size.store(size, memory_order_relaxed);
146 _slots.store(0, memory_order_relaxed);
148 _copy_idx.store(0, memory_order_relaxed);
149 _copy_done.store(0, memory_order_release);
156 // Heuristic to decide if the table is too full
157 bool table_full(int reprobe_cnt, int len) {
159 reprobe_cnt >= REPROBE_LIMIT &&
160 _slots.load(memory_order_relaxed) >= reprobe_limit(len);
163 kvs_data* resize(cliffc_hashtable *topmap, kvs_data *kvs) {
164 kvs_data *newkvs = _newkvs.load(memory_order_acquire);
168 // No copy in-progress, start one; Only double the table size
169 int oldlen = kvs->_size;
170 int sz = _size.load(memory_order_relaxed);
173 // Just follow Cliff Click's heuristic to decide the new size
174 if (sz >= (oldlen >> 2)) { // If we are 25% full
175 newsz = oldlen << 1; // Double size
176 if (sz >= (oldlen >> 1))
177 newsz = oldlen << 2; // Double double size
180 // We do not record the record timestamp
181 if (newsz <= oldlen) newsz = oldlen << 1;
182 // Do not shrink ever
183 if (newsz < oldlen) newsz = oldlen;
185 // Last check cause the 'new' below is expensive
186 newkvs = _newkvs.load(memory_order_acquire);
187 if (newkvs != NULL) return newkvs;
189 newkvs = new kvs_data(newsz);
190 void *chm = (void*) new CHM(sz);
191 newkvs->_data[0].store(chm, memory_order_relaxed);
193 kvs_data *cur_newkvs;
194 // Another check after the slow allocation
195 if ((cur_newkvs = _newkvs.load(memory_order_acquire)) != NULL)
197 // CAS the _newkvs to the allocated table
198 kvs_data *desired = (kvs_data*) NULL;
199 kvs_data *expected = (kvs_data*) newkvs;
200 if (!_newkvs.compare_exchange_strong(desired, expected, memory_order_release,
201 memory_order_release)) {
202 // Should clean the allocated area
204 newkvs = _newkvs.load(memory_order_acquire);
209 void help_copy_impl(cliffc_hashtable *topmap, kvs_data *oldkvs,
211 assert (get_chm(oldkvs) == this);
212 kvs_data *newkvs = _newkvs.load(memory_order_acquire);
213 int oldlen = oldkvs->_size;
214 int min_copy_work = oldlen > 1024 ? 1024 : oldlen;
216 // Just follow Cliff Click's code here
217 int panic_start = -1;
219 while (_copy_done.load(memory_order_acquire) < oldlen) {
220 copyidx = _copy_idx.load(memory_order_acquire);
221 if (panic_start == -1) { // No painc
222 copyidx = _copy_idx.load(memory_order_acquire);
223 while (copyidx < (oldlen << 1) &&
224 !_copy_idx.compare_exchange_strong(copyidx, copyidx +
225 min_copy_work, memory_order_release, memory_order_release))
226 copyidx = _copy_idx.load(memory_order_relaxed);
227 if (!(copyidx < (oldlen << 1)))
228 panic_start = copyidx;
231 // Now copy the chunk of work we claimed
233 for (int i = 0; i < min_copy_work; i++)
234 if (copy_slot(topmap, (copyidx + i) & (oldlen - 1), oldkvs,
238 copy_check_and_promote(topmap, oldkvs, workdone);
240 copyidx += min_copy_work;
241 if (!copy_all && panic_start == -1)
242 return; // We are done with the work we claim
244 copy_check_and_promote(topmap, oldkvs, 0); // See if we can promote
247 kvs_data* copy_slot_and_check(cliffc_hashtable *topmap, kvs_data
248 *oldkvs, int idx, void *should_help) {
249 kvs_data *newkvs = _newkvs.load(memory_order_acquire);
250 // We're only here cause the caller saw a Prime
251 if (copy_slot(topmap, idx, oldkvs, _newkvs))
252 copy_check_and_promote(topmap, oldkvs, 1); // Record the slot copied
253 return (should_help == NULL) ? newkvs : topmap->help_copy(newkvs);
256 void copy_check_and_promote(cliffc_hashtable *topmap, kvs_data*
257 oldkvs, int workdone) {
258 int oldlen = oldkvs->_size;
259 int copyDone = _copy_done.load(memory_order_relaxed);
262 copyDone = _copy_done.load(memory_order_relaxed);
263 if (_copy_done.compare_exchange_weak(copyDone, copyDone +
264 workdone, memory_order_relaxed, memory_order_relaxed))
269 // Promote the new table to the current table
270 if (copyDone + workdone == oldlen &&
271 topmap->_kvs.load(memory_order_acquire) == oldkvs)
272 topmap->_kvs.compare_exchange_strong(oldkvs, _newkvs, memory_order_release,
273 memory_order_release);
276 bool copy_slot(cliffc_hashtable *topmap, int idx, kvs_data *oldkvs,
279 while ((key_slot = key(oldkvs, idx)) == NULL)
280 CAS_key(oldkvs, idx, NULL, TOMBSTONE);
282 // First CAS old to Prime
283 slot *oldval = val(oldkvs, idx, NULL);
284 while (!is_prime(oldval)) {
285 slot *box = (oldval == NULL || oldval == TOMBSTONE)
286 ? TOMBPRIME : new slot(true, oldval->_ptr);
287 if (CAS_val(oldkvs, idx, oldval, box)) {
288 if (box == TOMBPRIME)
289 return 1; // Copy done
290 // Otherwise we CAS'd the box
291 oldval = box; // Record updated oldval
294 oldval = val(oldkvs, idx, NULL); // Else re-try
297 if (oldval == TOMBPRIME) return false; // Copy already completed here
299 slot *old_unboxed = new slot(false, oldval->_ptr);
300 int copied_into_new = (putIfMatch(topmap, newkvs, key_slot, old_unboxed,
303 // Old value is exposed in the new table
304 while (!CAS_val(oldkvs, idx, oldval, TOMBPRIME))
305 oldval = val(oldkvs, idx, NULL);
307 return copied_into_new;
314 static const int Default_Init_Size = 8; // Intial table size
316 static slot* const MATCH_ANY;
317 static slot* const NO_MATCH_OLD;
319 static slot* const TOMBPRIME;
320 static slot* const TOMBSTONE;
322 static const int REPROBE_LIMIT = 10; // Forces a table-resize
324 atomic<kvs_data*> _kvs;
328 // Should initialize the CHM for the construction of the table
329 // For other CHM in kvs_data, they should be initialzed in resize()
330 // because the size is determined dynamically
331 kvs_data *kvs = new kvs_data(Default_Init_Size);
332 void *chm = (void*) new CHM(0);
333 kvs->_data[0].store(chm, memory_order_relaxed);
334 _kvs.store(kvs, memory_order_release);
337 cliffc_hashtable(int init_size) {
338 // Should initialize the CHM for the construction of the table
339 // For other CHM in kvs_data, they should be initialzed in resize()
340 // because the size is determined dynamically
341 kvs_data *kvs = new kvs_data(init_size);
342 void *chm = (void*) new CHM(0);
343 kvs->_data[0].store(chm, memory_order_relaxed);
344 _kvs.store(kvs, memory_order_release);
351 Get = Read_Val_Point1 | Read_Val_Point2 | Read_Val_Point3
354 TypeV *_Old_Val = __map.get(key);
356 _equals_val(_Old_Val, __RET__)
359 shared_ptr<TypeV> get(TypeK& key) {
360 void *key_ptr = (void*) new TypeK(key);
361 slot *key_slot = new slot(false, shared_ptr<void>(key_ptr));
362 int fullhash = hash(key_slot);
363 slot *V = get_impl(this, _kvs, key_slot, fullhash);
364 if (V == NULL) return NULL;
365 assert (!is_prime(V));
366 return static_pointer_cast<TypeV>(V->_ptr);
373 Put = Write_Val_Point
376 # Remember this old value at checking point
377 TypeV *_Old_Val = __map.get(key);
378 __map.put(key, &val);
380 _equals_val(__RET__, _Old_Val)
383 shared_ptr<TypeV> put(TypeK& key, TypeV& val) {
384 return putIfMatch(key, val, NO_MATCH_OLD);
389 @Interface: PutIfAbsent
391 Write_Val_Point | PutIfAbsent_Fail_Point
392 @Condition: __map.get(key) == NULL
395 TypeV *_Old_Val = __map.get(key);
397 __map.put(key, &value);
400 COND_SAT ? __RET__ == NULL : _equals_val(_Old_Val, __RET__)
403 shared_ptr<TypeV> putIfAbsent(TypeK& key, TypeV& value) {
404 return putIfMatch(key, val, TOMBSTONE);
409 @Interface: RemoveAny
410 @Commit_point_set: Write_Val_Point
413 TypeV *_Old_Val = __map.get(key);
414 __map.put(key, NULL);
416 _equals_val(__RET__, _Old_Val)
419 shared_ptr<TypeV> remove(TypeK& key) {
420 return putIfMatch(key, TOMBSTONE, NO_MATCH_OLD);
425 @Interface: RemoveIfMatcfh
427 Write_Val_Point | RemoveIfMatch_Fail_Point
429 _equals_val(__map.get(key), &val)
433 __map.put(key, NULL);
435 COND_SAT ? __RET__ : !__RET__
438 bool remove(TypeK& key, TypeV& val) {
439 slot *val_slot = val == NULL ? NULL : new slot(false, val);
440 return putIfMatch(key, TOMBSTONE, val) == val;
446 @Interface: ReplaceAny
451 TypeV *_Old_Val = __map.get(key);
453 _equals_val(__RET__, _Old_Val)
456 shared_ptr<TypeV> replace(TypeK& key, TypeV& val) {
457 return putIfMatch(key, val, MATCH_ANY);
462 @Interface: ReplaceIfMatch
464 Write_Val_Point | ReplaceIfMatch_Fail_Point
466 _equals_val(__map.get(key), &oldval)
470 __map.put(key, &newval);
472 COND_SAT ? __RET__ : !__RET__
475 bool replace(TypeK& key, TypeV& oldval, TypeV& newval) {
476 return putIfMatch(key, newval, oldval) == oldval;
480 static CHM* get_chm(kvs_data* kvs) {
481 return (CHM*) kvs->_data[0].load(memory_order_relaxed);
484 static int* get_hashes(kvs_data *kvs) {
485 return (int *) kvs->_data[1].load(memory_order_relaxed);
488 // Preserve happens-before semantics on newly inserted keys
489 static inline slot* key(kvs_data *kvs, int idx) {
490 assert (idx >= 0 && idx < kvs->_size);
491 // Corresponding to the volatile read in get_impl() and putIfMatch in
492 // Cliff Click's Java implementation
493 return (slot*) kvs->_data[idx * 2 + 2].load(memory_order_acquire);
497 The atomic operation in val() function is a "potential" commit point,
498 which means in some case it is a real commit point while it is not for
499 some other cases. This so happens because the val() function is such a
500 fundamental function that many internal operation will call. Our
501 strategy is that we label any potential commit points and check if they
502 really are the commit points later.
504 // Preserve happens-before semantics on newly inserted values
505 static inline slot* val(kvs_data *kvs, int idx) {
506 assert (idx >= 0 && idx < kvs->_size);
507 // Corresponding to the volatile read in get_impl() and putIfMatch in
508 // Cliff Click's Java implementation
509 slot *res = (slot*) kvs->_data[idx * 2 + 3].load(memory_order_acquire);
512 # This is a complicated potential commit point since many many functions are
514 @Potential_commit_point_define: true
515 @Label: Read_Val_Point
523 static int hash(slot *key_slot) {
524 assert(key_slot != NULL && key_slot->_ptr != NULL);
525 shared_ptr<TypeK> key = static_pointer_cast<TypeK>(key_slot->_ptr);
526 int h = key->hashCode();
527 // Spread bits according to Cliff Click's code
528 h += (h << 15) ^ 0xffffcd7d;
532 h += (h << 2) + (h << 14);
533 return h ^ (h >> 16);
536 // Heuristic to decide if reprobed too many times.
537 // Be careful here: Running over the limit on a 'get' acts as a 'miss'; on a
538 // put it triggers a table resize. Several places MUST have exact agreement.
539 static int reprobe_limit(int len) {
540 return REPROBE_LIMIT + (len >> 2);
543 static inline bool is_prime(slot *val) {
544 return (val != NULL) && val->_prime;
547 // Check for key equality. Try direct pointer comparison first (fast
548 // negative teset) and then the full 'equals' call
549 static bool keyeq(slot *K, slot *key_slot, int *hashes, int hash,
551 // Caller should've checked this.
553 shared_ptr<TypeK> key_ptr = static_pointer_cast<TypeK>(key_slot->_ptr);
556 ((hashes[hash] == 0 || hashes[hash] == fullhash) &&
558 key_ptr->equals(K->_ptr));
561 static bool valeq(slot *val_slot1, slot *val_slot2) {
562 assert (val_slot1 != NULL);
563 shared_ptr<TypeK> ptr1 = static_pointer_cast<TypeV>(val_slot1->_ptr);
564 if (val_slot2 == NULL || ptr1 == NULL) return false;
565 return ptr1->equals(val_slot2->_ptr);
568 // Together with key() preserve the happens-before relationship on newly
570 static inline bool CAS_key(kvs_data *kvs, int idx, void *expected, void *desired) {
571 return kvs->_data[2 * idx + 2].compare_exchange_strong(expected,
572 desired, memory_order_release, memory_order_release);
576 Same as the val() function, we only label the CAS operation as the
577 potential commit point.
579 // Together with val() preserve the happens-before relationship on newly
581 static inline bool CAS_val(kvs_data *kvs, int idx, void *expected, void
583 bool res = kvs->_data[2 * idx + 3].compare_exchange_strong(expected,
584 desired, memory_order_release, memory_order_release);
586 # If it is a successful put instead of a copy or any other internal
587 # operantions, expected != NULL
589 @Potential_commit_point_define: __ATOMIC_RET__ == true
590 @Label: Write_Val_Point
596 slot* get_impl(cliffc_hashtable *topmap, kvs_data *kvs, slot* key_slot, int
598 int len = kvs->_size;
599 CHM *chm = get_chm(kvs);
600 int *hashes = get_hashes(kvs);
602 int idx = fullhash & (len - 1);
605 slot *K = key(kvs, idx);
606 slot *V = val(kvs, idx);
609 @Commit_point_define: V == NULL
610 @Potential_commit_point_label: Read_Val_Point
611 @Label: Get_Success_Point_1
615 if (V == NULL) return NULL; // A miss
617 if (keyeq(K, key_slot, hashes, idx, fullhash)) {
618 // Key hit! Check if table-resize in progress
622 @Commit_point_define: true
623 @Potential_commit_point_label: Read_Val_Point
624 @Label: Get_Success_Point_2
627 return (V == TOMBSTONE) ? NULL : V; // Return this value
629 // Otherwise, finish the copy & retry in the new table
630 return get_impl(topmap, chm->copy_slot_and_check(topmap, kvs,
631 idx, key_slot), key_slot, fullhash);
634 if (++reprobe_cnt >= REPROBE_LIMIT ||
635 key_slot == TOMBSTONE) {
636 // Retry in new table
637 // Atomic read (acquire) can be here
638 kvs_data *newkvs = chm->_newkvs.load(memory_order_acquire);
641 @Commit_point_define_check: newkvs == NULL
642 @Label: Get_Success_Point_3
645 return newkvs == NULL ? NULL : get_impl(topmap,
646 topmap->help_copy(newkvs), key_slot, fullhash);
649 idx = (idx + 1) & (len - 1); // Reprobe by 1
653 // A wrapper of the essential function putIfMatch()
654 shared_ptr<TypeV> putIfMatch(TypeK& key, TypeV& value, slot *old_val) {
655 // TODO: Should throw an exception rather return NULL
656 if (old_val == NULL) {
659 void *key_ptr = (void*) new TypeK(key);
660 slot *key_slot = new slot(false, shared_ptr<void>(key_ptr));
662 void *val_ptr = (void*) new TypeV(value);
663 slot *value_slot = new slot(false, shared_ptr<void>(val_ptr));
664 slot *res = putIfMatch(this, _kvs, key_slot, value_slot, old_val);
665 // Only when copy_slot() call putIfMatch() will it return NULL
666 assert (res != NULL);
667 assert (!is_prime(res));
668 return res == TOMBSTONE ? NULL : static_pointer_cast<TypeV>(res->_ptr);
672 Put, Remove, PutIfAbsent, etc will call this function. Return the old
673 value. If the returned value is equals to the expVal (or expVal is
674 NO_MATCH_OLD), then this function puts the val_slot to the table 'kvs'.
675 Only copy_slot will pass a NULL expVal, and putIfMatch only returns a
676 NULL if passed a NULL expVal.
678 static slot* putIfMatch(cliffc_hashtable *topmap, kvs_data *kvs, slot
679 *key_slot, slot *val_slot, slot *expVal) {
680 assert (val_slot != NULL);
681 assert (!is_prime(val_slot));
682 assert (!is_prime(expVal));
684 int fullhash = hash(key_slot);
685 int len = kvs->_size;
686 CHM *chm = get_chm(kvs);
687 int *hashes = get_hashes(kvs);
688 int idx = fullhash & (len - 1);
696 while (true) { // Spin till we get a key slot
698 V = val(kvs, idx, NULL);
699 if (K == NULL) { // Get a free slot
700 if (val_slot == TOMBSTONE) return val_slot;
701 // Claim the null key-slot
702 if (CAS_key(kvs, idx, NULL, key_slot)) {
703 chm->_slots.fetch_add(1, memory_order_relaxed); // Inc key-slots-used count
704 hashes[idx] = fullhash; // Memorize full hash
707 K = key(kvs, idx); // CAS failed, get updated value
711 // Key slot not null, there exists a Key here
712 if (keyeq(K, key_slot, hashes, idx, fullhash))
715 // Notice that the logic here should be consistent with that of get.
716 // The first predicate means too many reprobes means nothing in the
718 if (++reprobe_cnt >= reprobe_limit(len) ||
719 K == TOMBSTONE) { // Found a Tombstone key, no more keys
720 newkvs = chm->resize(topmap, kvs);
721 // Help along an existing copy
722 if (expVal != NULL) topmap->help_copy(newkvs);
723 return putIfMatch(topmap, newkvs, key_slot, val_slot, expVal);
726 idx = (idx + 1) & (len - 1); // Reprobe
727 } // End of spinning till we get a Key slot
729 if (val_slot == V) return V; // Fast cutout for no-change
731 // Here it tries to resize cause it doesn't want other threads to stop
732 // its progress (eagerly try to resize soon)
733 newkvs = chm->_newkvs.load(memory_order_acquire);
734 if (newkvs == NULL &&
735 ((V == NULL && chm->table_full(reprobe_cnt, len)) || is_prime(V)))
736 newkvs = chm->resize(topmap, kvs); // Force the copy to start
738 // Finish the copy and then put it in the new table
740 return putIfMatch(topmap, chm->copy_slot_and_check(topmap, kvs, idx,
741 expVal), key_slot, val_slot, expVal);
743 // Decided to update the existing table
745 assert (!is_prime(V));
747 if (expVal != NO_MATCH_OLD &&
749 (expVal != MATCH_ANY || V == TOMBSTONE || V == NULL) &&
750 !(V == NULL && expVal == TOMBSTONE) &&
751 (expVal == NULL || !valeq(expVal, V))) {
754 @Commit_point_define: expVal == TOMBSTONE
755 @Potential_commit_point_label: Read_Val_Point
756 @Label: PutIfAbsent_Fail_Point
757 # This is a check for the PutIfAbsent() when the value
762 @Commit_point_define: expVal != NULL && val_slot == TOMBSTONE
763 @Potential_commit_point_label: Read_Val_Point
764 @Label: RemoveIfMatch_Fail_Point
768 @Commit_point_define: !valeq(expVal, V)
769 @Potential_commit_point_label: Read_Val_Point
770 @Label: ReplaceIfMatch_Fail_Point
773 return V; // Do not update!
776 if (CAS_val(kvs, idx, V, val_slot)) {
779 # The only point where a successful put happens
780 @Commit_point_define: true
781 @Potential_commit_point_label: Write_Val_Point
782 @Label: Write_Success_Point
785 if (expVal != NULL) { // Not called by a table-copy
786 // CAS succeeded, should adjust size
787 // Both normal put's and table-copy calls putIfMatch, but
788 // table-copy does not increase the number of live K/V pairs
789 if ((V == NULL || V == TOMBSTONE) &&
790 val_slot != TOMBSTONE)
791 chm->_size.fetch_add(1, memory_order_relaxed);
792 if (!(V == NULL || V == TOMBSTONE) &&
793 val_slot == TOMBSTONE)
794 chm->_size.fetch_add(-1, memory_order_relaxed);
796 return (V == NULL && expVal != NULL) ? TOMBSTONE : V;
799 V = val(kvs, idx, NULL);
801 return putIfMatch(topmap, chm->copy_slot_and_check(topmap, kvs,
802 idx, expVal), key_slot, val_slot, expVal);
806 // Help along an existing table-resize. This is a fast cut-out wrapper.
807 kvs_data* help_copy(kvs_data *helper) {
808 kvs_data *topkvs = _kvs.load(memory_order_acquire);
809 CHM *topchm = get_chm(topkvs);
810 // No cpy in progress
811 if (topchm->_newkvs.load(memory_order_acquire) == NULL) return helper;
812 topchm->help_copy_impl(this, topkvs, false);