Skip to content

Commit a236ebd

Browse files
committed
Implement the BRAVO biased reader/writer lock
The goal of the BRAVO biased rwlock is to avoid readers taking the reader lock and thus contending for the atomic variable. Instead, readers raise a flag in an array to signal that they "took the lock" to any future writer. A writer takes the underlying atomic lock and waits for all readers to complete. While a writer has the lock, readers wait for the atomic rwlock. The hash table in PaRSEC is a prime example for a use-case: writers are extremely rare since the resizing happens rarely and the max size is capped. However, every thread locking a bucket also takes a reader-lock. We can thus avoid the contention on the global lock for most of the application run. The original proposal used a global hash table for all locks (https://arxiv.org/abs/1810.01553) but we use one array per lock. We know the number of threads in PaRSEC and can use fixed offsets, with padding to prevent cache line sharing (64B per thread). If an unknown thread takes the lock it goes straight to the atomic rwlock (unfortuntely, this includes the MPI progress thread at the moment). Signed-off-by: Joseph Schuchart <schuchart@icl.utk.edu>
1 parent 5f72c29 commit a236ebd

5 files changed

Lines changed: 208 additions & 15 deletions

File tree

parsec/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ add_compile_options("$<$<NOT:$<COMPILE_LANGUAGE:Fortran>>:${PARSEC_ATOMIC_SUPPOR
77
# Settings for targets
88
#
99
set(BASE_SOURCES
10+
class/parsec_biased_rwlock.c
1011
class/parsec_dequeue.c
1112
class/parsec_fifo.c
1213
class/parsec_lifo.c
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/*
2+
* Copyright (c) 2009-2022 The University of Tennessee and The University
3+
* of Tennessee Research Foundation. All rights
4+
* reserved.
5+
*/
6+
7+
#include "parsec/class/parsec_biased_rwlock.h"
8+
9+
#include <assert.h>
10+
11+
#include "parsec/runtime.h"
12+
#include "parsec/constants.h"
13+
#include "parsec/execution_stream.h"
14+
#include "parsec/sys/atomic.h"
15+
#include "parsec/class/parsec_rwlock.h"
16+
17+
/**
18+
* An implementation of the BRAVO biased reader/writer lock wrapper.
19+
* The goal of the BRAVO lock wrapper is to avoid contending the atomic
20+
* rwlock with reader locks, instead having threads mark their read status
21+
* is an array. A writer will first take the rwlock, signal that a writer
22+
* is active, and then wait for all readers to complete. New readers will
23+
* see that a writer is active and wait for the reader lock to become available.
24+
*
25+
* This is clearly biased towards readers so this implementation is meant for
26+
* cases where the majority of accesses is reading and only occasional writes occur.
27+
*
28+
* The paper presenting this technique is available at:
29+
* https://arxiv.org/abs/1810.01553
30+
*
31+
* While the original implementation uses a global hash table, we use a smaller table
32+
* per lock. In PaRSEC, we know the number of threads we control up front.
33+
* We simply pad for a cache line. If an unknown thread tries to take the lock against
34+
* all odds, it falls back to taking the reader lock.
35+
*/
36+
37+
struct parsec_biased_rwlock_t {
38+
parsec_atomic_rwlock_t rw_lock; /**< underlying reader-writer lock */
39+
int32_t reader_bias; /**< whether locking is biased towards readers, will change if a writer occurs */
40+
uint32_t num_reader; /**< size of the reader_active field */
41+
uint8_t reader_active[]; /**< array with flags signalling reading threads */
42+
};
43+
44+
#define DEFAULT_CACHE_SIZE 64
45+
46+
int parsec_biased_rwlock_init(parsec_biased_rwlock_t **lock) {
47+
parsec_biased_rwlock_t *res;
48+
parsec_execution_stream_t *es = parsec_my_execution_stream();
49+
if (NULL == es) {
50+
/* should be called from a parsec thread */
51+
res = (parsec_biased_rwlock_t *)malloc(sizeof(parsec_biased_rwlock_t));
52+
res->num_reader = 0;
53+
res->reader_bias = 0; // disable reader biasing
54+
} else {
55+
uint32_t num_threads = es->virtual_process->nb_cores;
56+
/* one cache line per reader */
57+
uint32_t num_reader = num_threads*DEFAULT_CACHE_SIZE;
58+
res = (parsec_biased_rwlock_t *)malloc(sizeof(parsec_biased_rwlock_t) + num_reader*sizeof(uint8_t));
59+
parsec_atomic_rwlock_init(&res->rw_lock);
60+
res->reader_bias = 1;
61+
res->num_reader = num_reader;
62+
memset(res->reader_active, 0, num_reader);
63+
}
64+
*lock = res;
65+
66+
return PARSEC_SUCCESS;
67+
}
68+
69+
void parsec_biased_rwlock_rdlock(parsec_biased_rwlock_t *lock)
70+
{
71+
parsec_execution_stream_t *es = parsec_my_execution_stream();
72+
if (PARSEC_UNLIKELY(NULL == es || lock->num_reader == 0)) {
73+
/* fall back to the underlying rwlock */
74+
parsec_atomic_rwlock_rdlock(&lock->rw_lock);
75+
return;
76+
}
77+
78+
if (PARSEC_UNLIKELY(!lock->reader_bias)) {
79+
/* a writer is active, wait for the rwlock to become available */
80+
parsec_atomic_rwlock_rdlock(&lock->rw_lock);
81+
return;
82+
}
83+
84+
/* fast-path: no writer, simply mark as active reader and make sure there is no race */
85+
size_t reader_entry = es->th_id*DEFAULT_CACHE_SIZE;
86+
assert(reader_entry >= 0 && reader_entry < lock->num_reader);
87+
assert(lock->reader_active[reader_entry] == 0);
88+
89+
lock->reader_active[reader_entry] = 1;
90+
/* make sure the writer check is not moved to before setting the flag */
91+
parsec_atomic_rmb();
92+
/* double check that no writer came in between */
93+
if (PARSEC_UNLIKELY(!lock->reader_bias)) {
94+
/* a writer has become active, fallback to the rwlock */
95+
lock->reader_active[reader_entry] = 0;
96+
parsec_atomic_rwlock_rdlock(&lock->rw_lock);
97+
}
98+
}
99+
100+
void parsec_biased_rwlock_rdunlock(parsec_biased_rwlock_t *lock)
101+
{
102+
parsec_execution_stream_t *es = parsec_my_execution_stream();
103+
104+
if (PARSEC_UNLIKELY(NULL == es || lock->num_reader == 0)) {
105+
/* fall back to the underlying rwlock */
106+
parsec_atomic_rwlock_rdunlock(&lock->rw_lock);
107+
return;
108+
}
109+
110+
size_t reader_entry = es->th_id*DEFAULT_CACHE_SIZE;
111+
assert(reader_entry >= 0 && reader_entry < lock->num_reader);
112+
113+
if (PARSEC_UNLIKELY(lock->reader_active[reader_entry] == 0)) {
114+
/* we had to take a lock, give it back */
115+
parsec_atomic_rwlock_rdunlock(&lock->rw_lock);
116+
} else {
117+
lock->reader_active[reader_entry] = 0;
118+
}
119+
}
120+
121+
void parsec_biased_rwlock_wrlock(parsec_biased_rwlock_t *lock)
122+
{
123+
/* acquire the writer lock first */
124+
parsec_atomic_rwlock_wrlock(&lock->rw_lock);
125+
126+
lock->reader_bias = 0;
127+
128+
/* make sure the reads below are not moved before the write */
129+
parsec_atomic_wmb();
130+
131+
/* wait for all current reader to complete */
132+
for (uint32_t i = 0; i < lock->num_reader; ++i) {
133+
while (lock->reader_active[i] != 0) {
134+
static struct timespec ts = { .tv_sec = 0, .tv_nsec = 100 };
135+
nanosleep(&ts, NULL);
136+
}
137+
}
138+
}
139+
140+
void parsec_biased_rwlock_wrunlock(parsec_biased_rwlock_t *lock)
141+
{
142+
assert(lock->reader_bias == 0);
143+
if (lock->num_reader > 0) {
144+
/* re-enable reader bias, if we support it */
145+
lock->reader_bias = 1;
146+
}
147+
parsec_atomic_rwlock_wrunlock(&lock->rw_lock);
148+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright (c) 2009-2022 The University of Tennessee and The University
3+
* of Tennessee Research Foundation. All rights
4+
* reserved.
5+
*/
6+
#ifndef _parsec_biased_rwlock_h
7+
#define _parsec_biased_rwlock_h
8+
9+
#include "parsec/parsec_config.h"
10+
11+
/**
12+
* An implementation of the BRAVO biased reader/writer lock wrapper.
13+
* The goal of the BRAVO lock wrapper is to avoid contending the atomic
14+
* rwlock with reader locks, instead having threads mark their read status
15+
* is an array. A writer will first take the rwlock, signal that a writer
16+
* is active, and then wait for all readers to complete. New readers will
17+
* see that a writer is active and wait for the reader lock to become available.
18+
*
19+
* This is clearly biased towards readers so this implementation is meant for
20+
* cases where the majority of accesses is reading and only occasional writes occur.
21+
*
22+
* The paper presenting this technique is available at:
23+
* https://arxiv.org/abs/1810.01553
24+
*
25+
* While the original implementation uses a global hash table, we use a smaller table
26+
* per lock. In PaRSEC, we know the number of threads we control up front.
27+
* We simply pad for a cache line. If an unknown thread tries to take the lock against
28+
* all odds, it falls back to taking the reader lock.
29+
*/
30+
31+
/* fwd-decl */
32+
typedef struct parsec_biased_rwlock_t parsec_biased_rwlock_t;
33+
34+
int parsec_biased_rwlock_init(parsec_biased_rwlock_t **lock);
35+
36+
void parsec_biased_rwlock_rdlock(parsec_biased_rwlock_t *lock);
37+
38+
void parsec_biased_rwlock_rdunlock(parsec_biased_rwlock_t *lock);
39+
40+
void parsec_biased_rwlock_wrlock(parsec_biased_rwlock_t *lock);
41+
42+
void parsec_biased_rwlock_wrunlock(parsec_biased_rwlock_t *lock);
43+
44+
#endif // _parsec_biased_rwlock_h

parsec/class/parsec_hash_table.c

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,6 @@ int parsec_hash_tables_init(void)
8989

9090
void parsec_hash_table_init(parsec_hash_table_t *ht, int64_t offset, int nb_bits, parsec_key_fn_t key_functions, void *data)
9191
{
92-
parsec_atomic_rwlock_t unlock = { PARSEC_RWLOCK_UNLOCKED };
9392
parsec_hash_table_head_t *head;
9493
size_t i;
9594
int v;
@@ -120,7 +119,7 @@ void parsec_hash_table_init(parsec_hash_table_t *ht, int64_t offset, int nb_bits
120119
head->next = NULL;
121120
head->next_to_free = NULL;
122121
ht->rw_hash = head;
123-
ht->rw_lock = unlock;
122+
parsec_biased_rwlock_init(&ht->rw_lock);
124123

125124
for( i = 0; i < (1ULL<<nb_bits); i++) {
126125
parsec_atomic_lock_init(&head->buckets[i].lock);
@@ -242,7 +241,7 @@ void parsec_hash_table_lock_bucket(parsec_hash_table_t *ht, parsec_key_t key )
242241
{
243242
uint64_t hash;
244243

245-
parsec_atomic_rwlock_rdlock(&ht->rw_lock);
244+
parsec_biased_rwlock_rdlock(ht->rw_lock);
246245
hash = parsec_hash_table_universal_rehash(ht->key_functions.key_hash(key, ht->hash_data), ht->rw_hash->nb_bits);
247246
assert( hash < (1ULL<<ht->rw_hash->nb_bits) );
248247
parsec_atomic_lock(&ht->rw_hash->buckets[hash].lock);
@@ -290,17 +289,17 @@ void parsec_hash_table_unlock_bucket_impl(parsec_hash_table_t *ht, parsec_key_t
290289
}
291290
cur_head = ht->rw_hash;
292291
parsec_atomic_unlock(&ht->rw_hash->buckets[hash].lock);
293-
parsec_atomic_rwlock_rdunlock(&ht->rw_lock);
292+
parsec_biased_rwlock_rdunlock(ht->rw_lock);
294293

295294
if( resize ) {
296-
parsec_atomic_rwlock_wrlock(&ht->rw_lock);
295+
parsec_biased_rwlock_wrlock(ht->rw_lock);
297296
if( cur_head == ht->rw_hash ) {
298297
/* Barring ABA problems, nobody resized the hash table;
299298
* Good enough hint that it's our role to do so */
300299
parsec_hash_table_resize(ht);
301300
}
302301
/* Otherwise, let's asssume somebody resized already */
303-
parsec_atomic_rwlock_wrunlock(&ht->rw_lock);
302+
parsec_biased_rwlock_wrunlock(ht->rw_lock);
304303
}
305304
}
306305

@@ -535,7 +534,7 @@ void parsec_hash_table_insert_impl(parsec_hash_table_t *ht, parsec_hash_table_it
535534
uint64_t hash;
536535
parsec_hash_table_head_t *cur_head;
537536
int resize = 0;
538-
parsec_atomic_rwlock_rdlock(&ht->rw_lock);
537+
parsec_biased_rwlock_rdlock(ht->rw_lock);
539538
cur_head = ht->rw_hash;
540539
hash = parsec_hash_table_universal_rehash(ht->key_functions.key_hash(item->key, ht->hash_data), ht->rw_hash->nb_bits);
541540
assert( hash < (1ULL<<ht->rw_hash->nb_bits) );
@@ -553,45 +552,45 @@ void parsec_hash_table_insert_impl(parsec_hash_table_t *ht, parsec_hash_table_it
553552
}
554553
}
555554
parsec_atomic_unlock(&ht->rw_hash->buckets[hash].lock);
556-
parsec_atomic_rwlock_rdunlock(&ht->rw_lock);
555+
parsec_biased_rwlock_rdunlock(ht->rw_lock);
557556

558557
if( resize ) {
559-
parsec_atomic_rwlock_wrlock(&ht->rw_lock);
558+
parsec_biased_rwlock_wrlock(ht->rw_lock);
560559
if( cur_head == ht->rw_hash ) {
561560
/* Barring ABA problems, nobody resized the hash table;
562561
* Good enough hint that it's our role to do so */
563562
parsec_hash_table_resize(ht);
564563
}
565564
/* Otherwise, let's asssume somebody resized already */
566-
parsec_atomic_rwlock_wrunlock(&ht->rw_lock);
565+
parsec_biased_rwlock_wrunlock(ht->rw_lock);
567566
}
568567
}
569568

570569
void *parsec_hash_table_find(parsec_hash_table_t *ht, parsec_key_t key)
571570
{
572571
uint64_t hash;
573572
void *ret;
574-
parsec_atomic_rwlock_rdlock(&ht->rw_lock);
573+
parsec_biased_rwlock_rdlock(ht->rw_lock);
575574
hash = parsec_hash_table_universal_rehash(ht->key_functions.key_hash(key, ht->hash_data), ht->rw_hash->nb_bits);
576575
assert( hash < (1ULL<<ht->rw_hash->nb_bits) );
577576
parsec_atomic_lock(&ht->rw_hash->buckets[hash].lock);
578577
ret = parsec_hash_table_nolock_find(ht, key);
579578
parsec_atomic_unlock(&ht->rw_hash->buckets[hash].lock);
580-
parsec_atomic_rwlock_rdunlock(&ht->rw_lock);
579+
parsec_biased_rwlock_rdunlock(ht->rw_lock);
581580
return ret;
582581
}
583582

584583
void *parsec_hash_table_remove(parsec_hash_table_t *ht, parsec_key_t key)
585584
{
586585
uint64_t hash;
587586
void *ret;
588-
parsec_atomic_rwlock_rdlock(&ht->rw_lock);
587+
parsec_biased_rwlock_rdlock(ht->rw_lock);
589588
hash = parsec_hash_table_universal_rehash(ht->key_functions.key_hash(key, ht->hash_data), ht->rw_hash->nb_bits);
590589
assert( hash < (1ULL<<ht->rw_hash->nb_bits) );
591590
parsec_atomic_lock(&ht->rw_hash->buckets[hash].lock);
592591
ret = parsec_hash_table_nolock_remove(ht, key);
593592
parsec_atomic_unlock(&ht->rw_hash->buckets[hash].lock);
594-
parsec_atomic_rwlock_rdunlock(&ht->rw_lock);
593+
parsec_biased_rwlock_rdunlock(ht->rw_lock);
595594
return ret;
596595
}
597596

parsec/class/parsec_hash_table.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include "parsec/sys/atomic.h"
1212
#include "parsec/class/list_item.h"
1313
#include "parsec/class/parsec_rwlock.h"
14+
#include "parsec/class/parsec_biased_rwlock.h"
1415

1516
/**
1617
* @defgroup parsec_internal_classes_hashtable Hash Tables
@@ -74,7 +75,7 @@ typedef struct parsec_hash_table_head_s {
7475
*/
7576
struct parsec_hash_table_s {
7677
parsec_object_t super; /**< A Hash Table is a PaRSEC object */
77-
parsec_atomic_rwlock_t rw_lock; /**< 'readers' are threads that manipulate rw_hash (add, delete, find)
78+
parsec_biased_rwlock_t *rw_lock; /**< 'readers' are threads that manipulate rw_hash (add, delete, find)
7879
* but do not resize it; 'writers' are threads that resize
7980
* rw_hash */
8081
int64_t elt_hashitem_offset; /**< Elements belonging to this hash table have a parsec_hash_table_item_t

0 commit comments

Comments
 (0)