#define dummy (&_dummy_struct)
+#define SET_LOOKKEY_FOUND 1
+#define SET_LOOKKEY_NO_MATCH 0
+#define SET_LOOKKEY_ERROR (-1)
+#define SET_LOOKKEY_CHANGED (-2)
+#define SET_LOOKKEY_EMPTY (-3)
+
+typedef int (*compare_func)(PySetObject *so, setentry *table, setentry *ep,
+ PyObject *key, Py_hash_t hash);
+
+#ifdef Py_GIL_DISABLED
+
+#define SET_IS_SHARED(so) _PyObject_GC_IS_SHARED(so)
+#define SET_MARK_SHARED(so) _PyObject_GC_SET_SHARED(so)
+
+static void
+ensure_shared_on_read(PySetObject *so)
+{
+ if (!_Py_IsOwnedByCurrentThread((PyObject *)so) && !SET_IS_SHARED(so)) {
+ // The first time we access a set from a non-owning thread we mark it
+ // as shared. This ensures that a concurrent resize operation will
+ // delay freeing the old entries using QSBR, which is necessary
+ // to safely allow concurrent reads without locking...
+ Py_BEGIN_CRITICAL_SECTION(so);
+ if (!SET_IS_SHARED(so)) {
+ SET_MARK_SHARED(so);
+ }
+ Py_END_CRITICAL_SECTION();
+ }
+}
+
+static inline Py_ALWAYS_INLINE int
+set_compare_threadsafe(PySetObject *so, setentry *table, setentry *ep,
+ PyObject *key, Py_hash_t hash)
+{
+ PyObject *startkey = FT_ATOMIC_LOAD_PTR_ACQUIRE(ep->key);
+ if (startkey == NULL) {
+ return SET_LOOKKEY_EMPTY;
+ }
+ if (startkey == key) {
+ return SET_LOOKKEY_FOUND;
+ }
+ Py_ssize_t ep_hash = FT_ATOMIC_LOAD_SSIZE_ACQUIRE(ep->hash);
+ if (ep_hash == hash) {
+ if (!_Py_TryIncrefCompare(&ep->key, startkey)) {
+ return SET_LOOKKEY_CHANGED;
+ }
+ int cmp = PyObject_RichCompareBool(startkey, key, Py_EQ);
+ Py_DECREF(startkey);
+ if (cmp < 0) {
+ return SET_LOOKKEY_ERROR;
+ }
+ if (table == FT_ATOMIC_LOAD_PTR_ACQUIRE(so->table) &&
+ startkey == FT_ATOMIC_LOAD_PTR_ACQUIRE(ep->key)) {
+ assert(cmp == SET_LOOKKEY_FOUND || cmp == SET_LOOKKEY_NO_MATCH);
+ return cmp;
+ }
+ else {
+ /* The set was mutated, restart */
+ return SET_LOOKKEY_CHANGED;
+ }
+ }
+ return SET_LOOKKEY_NO_MATCH;
+}
+
+#else
+
+#define SET_IS_SHARED(so) 0
+#define SET_MARK_SHARED(so)
+
+#endif
+
+static inline Py_ALWAYS_INLINE int
+set_compare_entry_lock_held(PySetObject *so, setentry *table, setentry *entry,
+ PyObject *key, Py_hash_t hash)
+{
+ _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(so);
+ if (entry->hash == 0 && entry->key == NULL)
+ return SET_LOOKKEY_EMPTY;
+ if (entry->hash == hash) {
+ PyObject *startkey = entry->key;
+ assert(startkey != dummy);
+ if (startkey == key)
+ return SET_LOOKKEY_FOUND;
+ if (PyUnicode_CheckExact(startkey)
+ && PyUnicode_CheckExact(key)
+ && unicode_eq(startkey, key))
+ return SET_LOOKKEY_FOUND;
+ table = so->table;
+ Py_INCREF(startkey);
+ int cmp = PyObject_RichCompareBool(startkey, key, Py_EQ);
+ Py_DECREF(startkey);
+ if (cmp < 0)
+ return SET_LOOKKEY_ERROR;
+ if (table != so->table || entry->key != startkey)
+ return SET_LOOKKEY_CHANGED;
+ if (cmp > 0)
+ return SET_LOOKKEY_FOUND;
+ }
+ return SET_LOOKKEY_NO_MATCH;
+}
+
+// This is similar to set_compare_entry_lock_held() but we don't need to
+// incref startkey before comparing and we don't need to check if the set has
+// changed. This also omits the PyUnicode_CheckExact() special case since it
+// doesn't help much for frozensets.
+static inline Py_ALWAYS_INLINE int
+set_compare_frozenset(PySetObject *so, setentry *table, setentry *ep,
+ PyObject *key, Py_hash_t hash)
+{
+ assert(PyFrozenSet_Check(so));
+ PyObject *startkey = ep->key;
+ if (startkey == NULL) {
+ return SET_LOOKKEY_EMPTY;
+ }
+ if (startkey == key) {
+ return SET_LOOKKEY_FOUND;
+ }
+ Py_ssize_t ep_hash = ep->hash;
+ if (ep_hash == hash) {
+ int cmp = PyObject_RichCompareBool(startkey, key, Py_EQ);
+ if (cmp < 0) {
+ return SET_LOOKKEY_ERROR;
+ }
+ assert(cmp == SET_LOOKKEY_FOUND || cmp == SET_LOOKKEY_NO_MATCH);
+ return cmp;
+ }
+ return SET_LOOKKEY_NO_MATCH;
+}
+
+static void
+set_zero_table(setentry *table, size_t size)
+{
+#ifdef Py_GIL_DISABLED
+ for (size_t i = 0; i < size; i++) {
+ setentry *entry = &table[i];
+ FT_ATOMIC_STORE_SSIZE_RELAXED(entry->hash, 0);
+ FT_ATOMIC_STORE_PTR_RELEASE(entry->key, NULL);
+ }
+#else
+ memset(table, 0, sizeof(setentry)*size);
+#endif
+}
/* ======================================================================== */
/* ======= Begin logic for probing the hash table ========================= */
/* This must be >= 1 */
#define PERTURB_SHIFT 5
-static setentry *
-set_lookkey(PySetObject *so, PyObject *key, Py_hash_t hash)
+static int
+set_do_lookup(PySetObject *so, setentry *table, size_t mask, PyObject *key,
+ Py_hash_t hash, setentry **epp, compare_func compare_entry)
{
- setentry *table;
setentry *entry;
size_t perturb = hash;
- size_t mask = so->mask;
size_t i = (size_t)hash & mask; /* Unsigned for defined overflow behavior */
int probes;
- int cmp;
-
- int frozenset = PyFrozenSet_CheckExact(so);
+ int status;
while (1) {
- entry = &so->table[i];
+ entry = &table[i];
probes = (i + LINEAR_PROBES <= mask) ? LINEAR_PROBES: 0;
do {
- if (entry->hash == 0 && entry->key == NULL)
- return entry;
- if (entry->hash == hash) {
- PyObject *startkey = entry->key;
- assert(startkey != dummy);
- if (startkey == key)
- return entry;
- if (PyUnicode_CheckExact(startkey)
- && PyUnicode_CheckExact(key)
- && unicode_eq(startkey, key))
- return entry;
- table = so->table;
- if (frozenset) {
- cmp = PyObject_RichCompareBool(startkey, key, Py_EQ);
- if (cmp < 0)
- return NULL;
- } else {
- // incref startkey because it can be removed from the set by the compare
- Py_INCREF(startkey);
- cmp = PyObject_RichCompareBool(startkey, key, Py_EQ);
- Py_DECREF(startkey);
- if (cmp < 0)
- return NULL;
- if (table != so->table || entry->key != startkey)
- return set_lookkey(so, key, hash);
+ status = compare_entry(so, table, entry, key, hash);
+ if (status != SET_LOOKKEY_NO_MATCH) {
+ if (status == SET_LOOKKEY_EMPTY) {
+ return SET_LOOKKEY_NO_MATCH;
}
- if (cmp > 0)
- return entry;
- mask = so->mask;
+ *epp = entry;
+ return status;
}
entry++;
} while (probes--);
perturb >>= PERTURB_SHIFT;
i = (i * 5 + 1 + perturb) & mask;
}
+ Py_UNREACHABLE();
}
static int set_table_resize(PySetObject *, Py_ssize_t);
if (freeslot == NULL)
goto found_unused;
FT_ATOMIC_STORE_SSIZE_RELAXED(so->used, so->used + 1);
- freeslot->key = key;
- freeslot->hash = hash;
+ FT_ATOMIC_STORE_SSIZE_RELAXED(freeslot->hash, hash);
+ FT_ATOMIC_STORE_PTR_RELEASE(freeslot->key, key);
return 0;
found_unused:
so->fill++;
FT_ATOMIC_STORE_SSIZE_RELAXED(so->used, so->used + 1);
- entry->key = key;
- entry->hash = hash;
+ FT_ATOMIC_STORE_SSIZE_RELAXED(entry->hash, hash);
+ FT_ATOMIC_STORE_PTR_RELEASE(entry->key, key);
if ((size_t)so->fill*5 < mask*3)
return 0;
return set_table_resize(so, so->used>50000 ? so->used*2 : so->used*4);
i = (i * 5 + 1 + perturb) & mask;
}
found_null:
- entry->key = key;
- entry->hash = hash;
+ FT_ATOMIC_STORE_SSIZE_RELAXED(entry->hash, hash);
+ FT_ATOMIC_STORE_PTR_RELEASE(entry->key, key);
}
/* ======== End logic for probing the hash table ========================== */
/* ======================================================================== */
+static int
+set_lookkey(PySetObject *so, PyObject *key, Py_hash_t hash, setentry **epp)
+{
+ int status;
+ if (PyFrozenSet_CheckExact(so)) {
+ status = set_do_lookup(so, so->table, so->mask, key, hash, epp,
+ set_compare_frozenset);
+ }
+ else {
+ Py_BEGIN_CRITICAL_SECTION(so);
+ do {
+ status = set_do_lookup(so, so->table, so->mask, key, hash, epp,
+ set_compare_entry_lock_held);
+ } while (status == SET_LOOKKEY_CHANGED);
+ Py_END_CRITICAL_SECTION();
+ }
+ assert(status == SET_LOOKKEY_FOUND ||
+ status == SET_LOOKKEY_NO_MATCH ||
+ status == SET_LOOKKEY_ERROR);
+ return status;
+}
+
+#ifdef Py_GIL_DISABLED
+static int
+set_lookkey_threadsafe(PySetObject *so, PyObject *key, Py_hash_t hash)
+{
+ int status;
+ setentry *entry;
+ if (PyFrozenSet_CheckExact(so)) {
+ status = set_do_lookup(so, so->table, so->mask, key, hash, &entry,
+ set_compare_frozenset);
+ assert(status == SET_LOOKKEY_FOUND ||
+ status == SET_LOOKKEY_NO_MATCH ||
+ status == SET_LOOKKEY_ERROR);
+ return status;
+ }
+ ensure_shared_on_read(so);
+ setentry *table = FT_ATOMIC_LOAD_PTR_ACQUIRE(so->table);
+ size_t mask = FT_ATOMIC_LOAD_SSIZE_RELAXED(so->mask);
+ if (table == NULL || table != FT_ATOMIC_LOAD_PTR_ACQUIRE(so->table)) {
+ return set_lookkey(so, key, hash, &entry);
+ }
+ status = set_do_lookup(so, table, mask, key, hash, &entry,
+ set_compare_threadsafe);
+ if (status == SET_LOOKKEY_CHANGED) {
+ return set_lookkey(so, key, hash, &entry);
+ }
+ assert(status == SET_LOOKKEY_FOUND ||
+ status == SET_LOOKKEY_NO_MATCH ||
+ status == SET_LOOKKEY_ERROR);
+ return status;
+}
+#endif
+
+static void free_entries(setentry *entries, size_t size, bool use_qsbr)
+{
+#ifdef Py_GIL_DISABLED
+ if (use_qsbr) {
+ _PyMem_FreeDelayed(entries, size * sizeof(setentry));
+ return;
+ }
+#endif
+ PyMem_Free(entries);
+}
+
/*
Restructure the table by allocating a new table and reinserting all
keys again. When entries have been deleted, the new table may
{
setentry *oldtable, *newtable, *entry;
Py_ssize_t oldmask = so->mask;
+ Py_ssize_t oldsize = (size_t)oldmask + 1;
size_t newmask;
int is_oldtable_malloced;
setentry small_copy[PySet_MINSIZE];
/* Make the set empty, using the new table. */
assert(newtable != oldtable);
- memset(newtable, 0, sizeof(setentry) * newsize);
- so->mask = newsize - 1;
- so->table = newtable;
+ set_zero_table(newtable, newsize);
+ FT_ATOMIC_STORE_PTR_RELEASE(so->table, NULL);
+ FT_ATOMIC_STORE_SSIZE_RELAXED(so->mask, newsize - 1);
/* Copy the data over; this is refcount-neutral for active entries;
dummy entries aren't copied over, of course */
}
}
+ FT_ATOMIC_STORE_PTR_RELEASE(so->table, newtable);
+
if (is_oldtable_malloced)
- PyMem_Free(oldtable);
+ free_entries(oldtable, oldsize, SET_IS_SHARED(so));
return 0;
}
static int
set_contains_entry(PySetObject *so, PyObject *key, Py_hash_t hash)
{
- setentry *entry;
-
- entry = set_lookkey(so, key, hash);
- if (entry != NULL)
- return entry->key != NULL;
- return -1;
+#ifdef Py_GIL_DISABLED
+ return set_lookkey_threadsafe(so, key, hash);
+#else
+ setentry *entry; // unused
+ return set_lookkey(so, key, hash, &entry);
+#endif
}
#define DISCARD_NOTFOUND 0
{
setentry *entry;
PyObject *old_key;
-
- entry = set_lookkey(so, key, hash);
- if (entry == NULL)
+ int status = set_lookkey(so, key, hash, &entry);
+ if (status < 0) {
return -1;
- if (entry->key == NULL)
+ }
+ if (status == SET_LOOKKEY_NO_MATCH) {
return DISCARD_NOTFOUND;
+ }
+ assert(status == SET_LOOKKEY_FOUND);
old_key = entry->key;
- entry->key = dummy;
- entry->hash = -1;
+ FT_ATOMIC_STORE_SSIZE_RELAXED(entry->hash, -1);
FT_ATOMIC_STORE_SSIZE_RELAXED(so->used, so->used - 1);
+ FT_ATOMIC_STORE_PTR_RELEASE(entry->key, dummy);
Py_DECREF(old_key);
return DISCARD_FOUND;
}
static void
set_empty_to_minsize(PySetObject *so)
{
- memset(so->smalltable, 0, sizeof(so->smalltable));
+ FT_ATOMIC_STORE_PTR_RELEASE(so->table, NULL);
+ set_zero_table(so->smalltable, PySet_MINSIZE);
so->fill = 0;
FT_ATOMIC_STORE_SSIZE_RELAXED(so->used, 0);
- so->mask = PySet_MINSIZE - 1;
- so->table = so->smalltable;
+ FT_ATOMIC_STORE_SSIZE_RELAXED(so->mask, PySet_MINSIZE - 1);
FT_ATOMIC_STORE_SSIZE_RELAXED(so->hash, -1);
+ FT_ATOMIC_STORE_PTR_RELEASE(so->table, so->smalltable);
}
static int
setentry *table = so->table;
Py_ssize_t fill = so->fill;
Py_ssize_t used = so->used;
+ Py_ssize_t oldsize = (size_t)so->mask + 1;
int table_is_malloced = table != so->smalltable;
setentry small_copy[PySet_MINSIZE];
}
if (table_is_malloced)
- PyMem_Free(table);
+ free_entries(table, oldsize, SET_IS_SHARED(so));
return 0;
}
PySetObject *so = _PySet_CAST(self);
setentry *entry;
Py_ssize_t used = so->used;
+ Py_ssize_t oldsize = (size_t)so->mask + 1;
/* bpo-31095: UnTrack is needed before calling any callbacks */
PyObject_GC_UnTrack(so);
}
}
if (so->table != so->smalltable)
- PyMem_Free(so->table);
+ free_entries(so->table, oldsize, SET_IS_SHARED(so));
Py_TYPE(so)->tp_free(so);
}
key = other_entry->key;
if (key != NULL) {
assert(so_entry->key == NULL);
- so_entry->key = Py_NewRef(key);
- so_entry->hash = other_entry->hash;
+ FT_ATOMIC_STORE_SSIZE_RELAXED(so_entry->hash, other_entry->hash);
+ FT_ATOMIC_STORE_PTR_RELEASE(so_entry->key, Py_NewRef(key));
}
}
so->fill = other->fill;
if (entry > limit)
entry = so->table;
}
- key = entry->key;
- entry->key = dummy;
- entry->hash = -1;
+ FT_ATOMIC_STORE_SSIZE_RELAXED(entry->hash, -1);
FT_ATOMIC_STORE_SSIZE_RELAXED(so->used, so->used - 1);
+ key = entry->key;
+ FT_ATOMIC_STORE_PTR_RELEASE(entry->key, dummy);
so->finger = entry - so->table + 1; /* next place to start */
return key;
}
Py_uhash_t hash;
if (FT_ATOMIC_LOAD_SSIZE_RELAXED(so->hash) != -1) {
- return FT_ATOMIC_LOAD_SSIZE_RELAXED(so->hash);
+ return FT_ATOMIC_LOAD_SSIZE_ACQUIRE(so->hash);
}
hash = frozenset_hash_impl(self);
- FT_ATOMIC_STORE_SSIZE_RELAXED(so->hash, hash);
+ FT_ATOMIC_STORE_SSIZE_RELEASE(so->hash, hash);
return hash;
}
return NULL;
assert (PyAnySet_Check(so));
- Py_ssize_t so_used = FT_ATOMIC_LOAD_SSIZE(so->used);
- Py_ssize_t si_used = FT_ATOMIC_LOAD_SSIZE(si->si_used);
+ Py_ssize_t so_used = FT_ATOMIC_LOAD_SSIZE_RELAXED(so->used);
+ Py_ssize_t si_used = FT_ATOMIC_LOAD_SSIZE_RELAXED(si->si_used);
if (si_used != so_used) {
PyErr_SetString(PyExc_RuntimeError,
"Set changed size during iteration");
setentry tab[PySet_MINSIZE];
Py_hash_t h;
+ setentry *a_table = a->table;
+ setentry *b_table = b->table;
+ FT_ATOMIC_STORE_PTR_RELEASE(a->table, NULL);
+ FT_ATOMIC_STORE_PTR_RELEASE(b->table, NULL);
+
t = a->fill; a->fill = b->fill; b->fill = t;
t = a->used;
FT_ATOMIC_STORE_SSIZE_RELAXED(a->used, b->used);
FT_ATOMIC_STORE_SSIZE_RELAXED(b->used, t);
- t = a->mask; a->mask = b->mask; b->mask = t;
+ t = a->mask;
+ FT_ATOMIC_STORE_SSIZE_RELAXED(a->mask, b->mask);
+ FT_ATOMIC_STORE_SSIZE_RELAXED(b->mask, t);
- u = a->table;
- if (a->table == a->smalltable)
+ u = a_table;
+ if (a_table == a->smalltable)
u = b->smalltable;
- a->table = b->table;
- if (b->table == b->smalltable)
- a->table = a->smalltable;
- b->table = u;
+ a_table = b_table;
+ if (b_table == b->smalltable)
+ a_table = a->smalltable;
+ b_table = u;
- if (a->table == a->smalltable || b->table == b->smalltable) {
+ if (a_table == a->smalltable || b_table == b->smalltable) {
memcpy(tab, a->smalltable, sizeof(tab));
memcpy(a->smalltable, b->smalltable, sizeof(tab));
memcpy(b->smalltable, tab, sizeof(tab));
FT_ATOMIC_STORE_SSIZE_RELAXED(a->hash, -1);
FT_ATOMIC_STORE_SSIZE_RELAXED(b->hash, -1);
}
+ if (!SET_IS_SHARED(b) && SET_IS_SHARED(a)) {
+ SET_MARK_SHARED(b);
+ }
+ if (!SET_IS_SHARED(a) && SET_IS_SHARED(b)) {
+ SET_MARK_SHARED(a);
+ }
+ FT_ATOMIC_STORE_PTR_RELEASE(a->table, a_table);
+ FT_ATOMIC_STORE_PTR_RELEASE(b->table, b_table);
}
/*[clinic input]
Py_RETURN_NONE;
}
-static int
-set_contains_lock_held(PySetObject *so, PyObject *key)
+int
+_PySet_Contains(PySetObject *so, PyObject *key)
{
- int rv;
+ assert(so);
- rv = set_contains_key(so, key);
- if (rv < 0) {
- if (!PySet_Check(key) || !PyErr_ExceptionMatches(PyExc_TypeError))
+ Py_hash_t hash = _PyObject_HashFast(key);
+ if (hash == -1) {
+ if (!PySet_Check(key) || !PyErr_ExceptionMatches(PyExc_TypeError)) {
+ set_unhashable_type(key);
return -1;
+ }
PyErr_Clear();
- Py_hash_t hash;
+ // Note that 'key' could be a set() or frozenset() object. Unlike most
+ // container types, set allows membership testing with a set key, even
+ // though it is not hashable.
Py_BEGIN_CRITICAL_SECTION(key);
hash = frozenset_hash_impl(key);
Py_END_CRITICAL_SECTION();
- rv = set_contains_entry(so, key, hash);
- }
- return rv;
-}
-
-int
-_PySet_Contains(PySetObject *so, PyObject *key)
-{
- assert(so);
-
- int rv;
- if (PyFrozenSet_CheckExact(so)) {
- rv = set_contains_lock_held(so, key);
- } else {
- Py_BEGIN_CRITICAL_SECTION(so);
- rv = set_contains_lock_held(so, key);
- Py_END_CRITICAL_SECTION();
}
- return rv;
+ return set_contains_entry(so, key, hash);
}
static int
}
/*[clinic input]
-@critical_section
@coexist
set.__contains__
so: setobject
static PyObject *
set___contains___impl(PySetObject *so, PyObject *key)
-/*[clinic end generated code: output=b44863d034b3c70e input=4a7d568459617f24]*/
+/*[clinic end generated code: output=b44863d034b3c70e input=cf4c72db704e4cf0]*/
{
long result;
- result = set_contains_lock_held(so, key);
+ result = _PySet_Contains(so, key);
if (result < 0)
return NULL;
return PyBool_FromLong(result);
frozenset___contains___impl(PySetObject *so, PyObject *key)
/*[clinic end generated code: output=2301ed91bc3a6dd5 input=2f04922a98d8bab7]*/
{
- long result;
-
- result = set_contains_lock_held(so, key);
- if (result < 0)
+ Py_hash_t hash = _PyObject_HashFast(key);
+ if (hash == -1) {
+ if (!PySet_Check(key) || !PyErr_ExceptionMatches(PyExc_TypeError)) {
+ set_unhashable_type(key);
+ return NULL;
+ }
+ PyErr_Clear();
+ Py_BEGIN_CRITICAL_SECTION(key);
+ hash = frozenset_hash_impl(key);
+ Py_END_CRITICAL_SECTION();
+ }
+ setentry *entry; // unused
+ int status = set_do_lookup(so, so->table, so->mask, key, hash, &entry,
+ set_compare_frozenset);
+ if (status < 0)
return NULL;
- return PyBool_FromLong(result);
+ return PyBool_FromLong(status);
}
/*[clinic input]