Ruby 3.5.0dev (2025-07-01 revision d3d249b9048b338535ae033acb3606abf174b2da)
ractor_safe_set.c (d3d249b9048b338535ae033acb3606abf174b2da)
1#include "internal.h"
2#include "internal/gc.h"
3#include "internal/ractor_safe_set.h"
4#include "ruby_atomic.h"
5#include "ruby/atomic.h"
6#include "vm_sync.h"
7
8enum ractor_safe_set_special_values {
9 RACTOR_SAFE_TABLE_EMPTY,
10 RACTOR_SAFE_TABLE_DELETED,
11 RACTOR_SAFE_TABLE_MOVED,
12 RACTOR_SAFE_TABLE_SPECIAL_VALUE_COUNT
13};
14
16 VALUE hash;
17 VALUE key;
18};
19
21 rb_atomic_t size;
22 unsigned int capacity;
23 unsigned int deleted_entries;
24 struct rb_ractor_safe_set_funcs *funcs;
25 struct ractor_safe_set_entry *entries;
26};
27
28static void
29ractor_safe_set_free(void *ptr)
30{
31 struct ractor_safe_set *set = ptr;
32 xfree(set->entries);
33}
34
35static size_t
36ractor_safe_set_size(const void *ptr)
37{
38 const struct ractor_safe_set *set = ptr;
39 return sizeof(struct ractor_safe_set) +
40 (set->capacity * sizeof(struct ractor_safe_set_entry));
41}
42
43static const rb_data_type_t ractor_safe_set_type = {
44 .wrap_struct_name = "VM/ractor_safe_set",
45 .function = {
46 .dmark = NULL,
47 .dfree = ractor_safe_set_free,
48 .dsize = ractor_safe_set_size,
49 },
50 .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_EMBEDDABLE
51};
52
54rb_ractor_safe_set_new(struct rb_ractor_safe_set_funcs *funcs, int capacity)
55{
56 struct ractor_safe_set *set;
57 VALUE obj = TypedData_Make_Struct(0, struct ractor_safe_set, &ractor_safe_set_type, set);
58 set->funcs = funcs;
59 set->entries = ZALLOC_N(struct ractor_safe_set_entry, capacity);
60 set->capacity = capacity;
61 return obj;
62}
63
65 int idx;
66 int d;
67 int mask;
68};
69
70static int
71ractor_safe_set_probe_start(struct ractor_safe_set_probe *probe, struct ractor_safe_set *set, VALUE hash)
72{
73 RUBY_ASSERT((set->capacity & (set->capacity - 1)) == 0);
74 probe->d = 0;
75 probe->mask = set->capacity - 1;
76 probe->idx = hash & probe->mask;
77 return probe->idx;
78}
79
80static int
81ractor_safe_set_probe_next(struct ractor_safe_set_probe *probe)
82{
83 probe->d++;
84 probe->idx = (probe->idx + probe->d) & probe->mask;
85 return probe->idx;
86}
87
88static void
89ractor_safe_set_try_resize_without_locking(VALUE old_set_obj, VALUE *set_obj_ptr)
90{
91 // Check if another thread has already resized.
92 if (RUBY_ATOMIC_VALUE_LOAD(*set_obj_ptr) != old_set_obj) {
93 return;
94 }
95
96 struct ractor_safe_set *old_set = RTYPEDDATA_GET_DATA(old_set_obj);
97
98 // This may overcount by up to the number of threads concurrently attempting to insert
99 // GC may also happen between now and the set being rebuilt
100 int expected_size = RUBY_ATOMIC_LOAD(old_set->size) - old_set->deleted_entries;
101
102 struct ractor_safe_set_entry *old_entries = old_set->entries;
103 int old_capacity = old_set->capacity;
104 int new_capacity = old_capacity * 2;
105 if (new_capacity > expected_size * 8) {
106 new_capacity = old_capacity / 2;
107 }
108 else if (new_capacity > expected_size * 4) {
109 new_capacity = old_capacity;
110 }
111
112 // May cause GC and therefore deletes, so must hapen first.
113 VALUE new_set_obj = rb_ractor_safe_set_new(old_set->funcs, new_capacity);
114 struct ractor_safe_set *new_set = RTYPEDDATA_GET_DATA(new_set_obj);
115
116 for (int i = 0; i < old_capacity; i++) {
117 struct ractor_safe_set_entry *entry = &old_entries[i];
118 VALUE key = RUBY_ATOMIC_VALUE_EXCHANGE(entry->key, RACTOR_SAFE_TABLE_MOVED);
119 RUBY_ASSERT(key != RACTOR_SAFE_TABLE_MOVED);
120
121 if (key < RACTOR_SAFE_TABLE_SPECIAL_VALUE_COUNT) continue;
122 if (rb_objspace_garbage_object_p(key)) continue;
123
124 VALUE hash = RUBY_ATOMIC_VALUE_LOAD(entry->hash);
125 if (hash == 0) {
126 // Either in-progress insert or extremely unlikely 0 hash.
127 // Re-calculate the hash.
128 hash = old_set->funcs->hash(key);
129 }
130 RUBY_ASSERT(hash == old_set->funcs->hash(key));
131
132 // Insert key into new_set.
133 struct ractor_safe_set_probe probe;
134 int idx = ractor_safe_set_probe_start(&probe, new_set, hash);
135
136 while (true) {
137 struct ractor_safe_set_entry *entry = &new_set->entries[idx];
138
139 if (entry->key == RACTOR_SAFE_TABLE_EMPTY) {
140 new_set->size++;
141
142 RUBY_ASSERT(new_set->size < new_set->capacity / 2);
143 RUBY_ASSERT(entry->hash == 0);
144
145 entry->key = key;
146 entry->hash = hash;
147 break;
148 }
149 else {
150 RUBY_ASSERT(entry->key >= RACTOR_SAFE_TABLE_SPECIAL_VALUE_COUNT);
151 }
152
153 idx = ractor_safe_set_probe_next(&probe);
154 }
155 }
156
157 RUBY_ATOMIC_VALUE_SET(*set_obj_ptr, new_set_obj);
158
159 RB_GC_GUARD(old_set_obj);
160}
161
162static void
163ractor_safe_set_try_resize(VALUE old_set_obj, VALUE *set_obj_ptr)
164{
165 RB_VM_LOCKING() {
166 ractor_safe_set_try_resize_without_locking(old_set_obj, set_obj_ptr);
167 }
168}
169
170VALUE
171rb_ractor_safe_set_find_or_insert(VALUE *set_obj_ptr, VALUE key, void *data)
172{
173 RUBY_ASSERT(key >= RACTOR_SAFE_TABLE_SPECIAL_VALUE_COUNT);
174
175 bool inserting = false;
176 VALUE set_obj;
177
178 retry:
179 set_obj = RUBY_ATOMIC_VALUE_LOAD(*set_obj_ptr);
180 RUBY_ASSERT(set_obj);
181 struct ractor_safe_set *set = RTYPEDDATA_GET_DATA(set_obj);
182
183 struct ractor_safe_set_probe probe;
184 VALUE hash = set->funcs->hash(key);
185 int idx = ractor_safe_set_probe_start(&probe, set, hash);
186
187 while (true) {
188 struct ractor_safe_set_entry *entry = &set->entries[idx];
189 VALUE curr_key = RUBY_ATOMIC_VALUE_LOAD(entry->key);
190
191 switch (curr_key) {
192 case RACTOR_SAFE_TABLE_EMPTY: {
193 // Not in set
194 if (!inserting) {
195 key = set->funcs->create(key, data);
196 RUBY_ASSERT(hash == set->funcs->hash(key));
197 inserting = true;
198 }
199
200 rb_atomic_t prev_size = RUBY_ATOMIC_FETCH_ADD(set->size, 1);
201
202 if (UNLIKELY(prev_size > set->capacity / 2)) {
203 ractor_safe_set_try_resize(set_obj, set_obj_ptr);
204
205 goto retry;
206 }
207
208 curr_key = RUBY_ATOMIC_VALUE_CAS(entry->key, RACTOR_SAFE_TABLE_EMPTY, key);
209 if (curr_key == RACTOR_SAFE_TABLE_EMPTY) {
210 RUBY_ATOMIC_VALUE_SET(entry->hash, hash);
211
212 RB_GC_GUARD(set_obj);
213 return key;
214 }
215 else {
216 // Entry was not inserted.
217 RUBY_ATOMIC_DEC(set->size);
218
219 // Another thread won the race, try again at the same location.
220 continue;
221 }
222 }
223 case RACTOR_SAFE_TABLE_DELETED:
224 break;
225 case RACTOR_SAFE_TABLE_MOVED:
226 // Wait
227 RB_VM_LOCKING();
228
229 goto retry;
230 default: {
231 VALUE curr_hash = RUBY_ATOMIC_VALUE_LOAD(entry->hash);
232 if ((curr_hash == hash || curr_hash == 0) && set->funcs->cmp(key, curr_key)) {
233 // We've found a match.
234 if (UNLIKELY(rb_objspace_garbage_object_p(curr_key))) {
235 // This is a weakref set, so after marking but before sweeping is complete we may find a matching garbage object.
236 // Skip it and mark it as deleted.
237 RUBY_ATOMIC_VALUE_CAS(entry->key, curr_key, RACTOR_SAFE_TABLE_DELETED);
238
239 // Fall through and continue our search.
240 }
241 else {
242 RB_GC_GUARD(set_obj);
243 return curr_key;
244 }
245 }
246
247 break;
248 }
249 }
250
251 idx = ractor_safe_set_probe_next(&probe);
252 }
253}
254
255VALUE
256rb_ractor_safe_set_delete_by_identity(VALUE set_obj, VALUE key)
257{
258 // Assume locking and barrier (which there is no assert for).
259 ASSERT_vm_locking();
260
261 struct ractor_safe_set *set = RTYPEDDATA_GET_DATA(set_obj);
262
263 VALUE hash = set->funcs->hash(key);
264
265 struct ractor_safe_set_probe probe;
266 int idx = ractor_safe_set_probe_start(&probe, set, hash);
267
268 while (true) {
269 struct ractor_safe_set_entry *entry = &set->entries[idx];
270 VALUE curr_key = RUBY_ATOMIC_VALUE_LOAD(entry->key);
271
272 switch (curr_key) {
273 case RACTOR_SAFE_TABLE_EMPTY:
274 // We didn't find our entry to delete.
275 return 0;
276 case RACTOR_SAFE_TABLE_DELETED:
277 break;
278 case RACTOR_SAFE_TABLE_MOVED:
279 rb_bug("rb_ractor_safe_set_delete_by_identity: moved entry");
280 break;
281 default:
282 if (key == curr_key) {
283 entry->key = RACTOR_SAFE_TABLE_DELETED;
284 set->deleted_entries++;
285 return curr_key;
286 }
287 break;
288 }
289
290 idx = ractor_safe_set_probe_next(&probe);
291 }
292}
293
294void
295rb_ractor_safe_set_foreach_with_replace(VALUE set_obj, int (*callback)(VALUE *key, void *data), void *data)
296{
297 // Assume locking and barrier (which there is no assert for).
298 ASSERT_vm_locking();
299
300 struct ractor_safe_set *set = RTYPEDDATA_GET_DATA(set_obj);
301
302 for (unsigned int i = 0; i < set->capacity; i++) {
303 VALUE key = set->entries[i].key;
304
305 switch (key) {
306 case RACTOR_SAFE_TABLE_EMPTY:
307 case RACTOR_SAFE_TABLE_DELETED:
308 continue;
309 case RACTOR_SAFE_TABLE_MOVED:
310 rb_bug("rb_ractor_safe_set_foreach_with_replace: moved entry");
311 break;
312 default: {
313 int ret = callback(&set->entries[i].key, data);
314 switch (ret) {
315 case ST_STOP:
316 return;
317 case ST_DELETE:
318 set->entries[i].key = RACTOR_SAFE_TABLE_DELETED;
319 break;
320 }
321 break;
322 }
323 }
324 }
325}
#define RUBY_ASSERT(...)
Asserts that the given expression is truthy if and only if RUBY_DEBUG is truthy.
Definition assert.h:219
Atomic operations.
#define RUBY_ATOMIC_VALUE_CAS(var, oldval, newval)
Identical to RUBY_ATOMIC_CAS, except it expects its arguments are VALUE.
Definition atomic.h:381
#define RUBY_ATOMIC_VALUE_SET(var, val)
Identical to RUBY_ATOMIC_SET, except it expects its arguments are VALUE.
Definition atomic.h:353
std::atomic< unsigned > rb_atomic_t
Type that is eligible for atomic operations.
Definition atomic.h:69
#define RUBY_ATOMIC_FETCH_ADD(var, val)
Atomically replaces the value pointed by var with the result of addition of val to the old value of v...
Definition atomic.h:93
#define RUBY_ATOMIC_VALUE_EXCHANGE(var, val)
Identical to RUBY_ATOMIC_EXCHANGE, except it expects its arguments are VALUE.
Definition atomic.h:367
#define RUBY_ATOMIC_DEC(var)
Atomically decrements the value pointed by var.
Definition atomic.h:198
#define RUBY_ATOMIC_LOAD(var)
Atomic load.
Definition atomic.h:150
#define xfree
Old name of ruby_xfree.
Definition xmalloc.h:58
#define ZALLOC_N
Old name of RB_ZALLOC_N.
Definition memory.h:401
#define RB_GC_GUARD(v)
Prevents premature destruction of local objects.
Definition memory.h:167
#define TypedData_Make_Struct(klass, type, data_type, sval)
Identical to TypedData_Wrap_Struct, except it allocates a new data region internally instead of takin...
Definition rtypeddata.h:497
This is the struct that holds necessary info for a struct.
Definition rtypeddata.h:203
const char * wrap_struct_name
Name of structs of this kind.
Definition rtypeddata.h:210
uintptr_t VALUE
Type that represents a Ruby object.
Definition value.h:40