ArrayOfItems
class, described in my previous post, A Lock-Free ... Linear Search? I strongly recommend that you read it before continuing.
ArrayOfItems
, this hash table class, which I called HashTable1
, is implemented using a simple giant array of key / value pairs.
struct Entry { mint_atomic32_t key; mint_atomic32_t value; }; Entry *m_entries;
HashTable1
no linked lists for resolving hash collisions outside the table; there is only the array itself. The zero key in the array indicates an empty entry, and the array itself is initialized with zeros. And just like in ArrayOfItems
, the values ​​are added and arranged in HashTable1
using a linear search.
ArrayOfItems
and HashTable1
is that ArrayOfItems
always starts a linear search with a zero index, while HashTable1
starts each linear search with an index calculated as a hash of a key . I chose MurmurHash3's integer finalizer as a hash function, since it is fast enough and well encodes integer data.
inline static uint32_t integerHash(uint32_t h) { h ^= h >> 16; h *= 0x85ebca6b; h ^= h >> 13; h *= 0xc2b2ae35; h ^= h >> 16; return h; }
SetItem
or GetItem
with the same key, the linear search will start with the same index, but when we pass different keys, the search will, in most cases, begin with completely different indexes. Thus, the values ​​are much better distributed over the array than in ArrayOfItems
, and it is SetItem
call SetItem
and GetItem
from several parallel threads.
HashTable1
uses a circular search, which means that when a SetItem
or GetItem
reaches the end of the array, it simply returns to zero index and continues the search. Since the array will never be filled, each search is guaranteed to end either by detecting the required key or by detecting a record with key 0, which means that the required key does not exist in the hash table. This technique is called open addressing with linear probing , and in my opinion, this is the most lock-free-friendly hash table of the existing ones. In fact, Dr. Click uses the same technique in his Java-free security table.
SetItem
. It passes through an array and stores the value in the first record, the key of which is 0 or coincides with the desired key. Its code is almost identical to the ArrayOfItems::SetItem
code described in the previous post . The differences are only the integer hash and bitwise “and” applied to idx
, which does not allow it to go beyond the bounds of the array.
void HashTable1::SetItem(uint32_t key, uint32_t value) { for (uint32_t idx = integerHash(key);; idx++) { idx &= m_arraySize - 1; uint32_t prevKey = mint_compare_exchange_strong_32_relaxed(&m_entries[idx].key, 0, key); if ((prevKey == 0) || (prevKey == key)) { mint_store_32_relaxed(&m_entries[idx].value, value); return; } } }
GetItem
code also almost coincides with ArrayOfItems::GetItem
except for minor modifications.
uint32_t HashTable1::GetItem(uint32_t key) { for (uint32_t idx = integerHash(key);; idx++) { idx &= m_arraySize - 1; uint32_t probedKey = mint_load_32_relaxed(&m_entries[idx].key); if (probedKey == key) return mint_load_32_relaxed(&m_entries[idx].value); if (probedKey == 0) return 0; } }
ArrayOfItems
for the same reasons as their counterparts in ArrayOfItems
: all operations with array elements are performed using atomic library functions, values ​​are assigned to keys using compare-and-swap (CAS) on keys, and all code is resilient against memory reordering. And again, for a better understanding, I advise you to refer to the previous post .
SetItem
, first checking whether a CAS is really necessary, and if not, not applying it. Thanks to this optimization, an example of the application that you find below works almost 20% faster.
void HashTable1::SetItem(uint32_t key, uint32_t value) { for (uint32_t idx = integerHash(key);; idx++) { idx &= m_arraySize - 1; // Load the key that was there. uint32_t probedKey = mint_load_32_relaxed(&m_entries[idx].key); if (probedKey != key) { // The entry was either free, or contains another key. if (probedKey != 0) continue; // Usually, it contains another key. Keep probing. // The entry was free. Now let's try to take it using a CAS. uint32_t prevKey = mint_compare_exchange_strong_32_relaxed(&m_entries[idx].key, 0, key); if ((prevKey != 0) && (prevKey != key)) continue; // Another thread just stole it from underneath us. // Either we just added the key, or another thread did. } // Store the value in this array entry. mint_store_32_relaxed(&m_entries[idx].value, value); return; } }
ArrayOfItems
, all operations with HashTable1
performed with weak (relaxed) memory ordering constraints. Therefore, if you want to make any data available to other threads by writing a flag in HashTable1
, it is necessary that this record has “ release semantics ”, which can be guaranteed by putting release fence (“release barrier”) immediately before instruction. Similarly, accessing GetItem
in the stream that wants to receive data needs to acquire a acquire fence (“acquisition barrier”).
// Shared variables char message[256]; HashTable1 collection; void PublishMessage() { // Write to shared memory non-atomically. strcpy(message, "I pity the fool!"); // Release fence: The only way to safely pass non-atomic data between threads using Mintomic. mint_thread_fence_release(); // Set a flag to indicate to other threads that the message is ready. collection.SetItem(SHARED_FLAG_KEY, 1) }
HashTable1
at work, I created another simplest application, very similar to the example from the previous post. Each time it chooses from two experiments:
HashTable1
will show remarkable performance. Perhaps, I should confirm this with measurements, but based on previous experience in measuring single - threaded hash-table performance, I can argue that you will not be able to create a faster, non-blocking hash table than HashTable1
. It may seem surprising, but the ArrayOfItems
that underlies it shows awful performance. Of course, as with any hash table, there is a non-zero risk to hash a large number of keys into one array index, and then the performance will be equal to the speed of ArrayOfItems
. But with a fairly large table and a good hash function, such as MurmurHash3, the likelihood of such a scenario is negligible.
Source: https://habr.com/ru/post/322496/