Skip to content

Commit 04d2fb7

Browse files
Merge branch 'main' into mimalloc
2 parents 40c33a5 + d1505b5 commit 04d2fb7

File tree

7 files changed

+105
-34
lines changed

7 files changed

+105
-34
lines changed

Include/internal/pycore_qsbr.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,9 @@ struct _qsbr_shared {
8383
// Minimum observed read sequence of all QSBR thread states
8484
uint64_t rd_seq;
8585

86-
// Array of QSBR thread states.
86+
// Array of QSBR thread states (aligned to 64 bytes).
8787
struct _qsbr_pad *array;
88+
void *array_raw; // raw allocation pointer (for free)
8889
Py_ssize_t size;
8990

9091
// Freelist of unused _qsbr_thread_states (protected by mutex)

Include/internal/pycore_tstate.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,12 @@ typedef struct _PyThreadStateImpl {
102102
#if _Py_TIER2
103103
struct _PyJitTracerState *jit_tracer_state;
104104
#endif
105+
106+
#ifdef Py_GIL_DISABLED
107+
// gh-144438: Add padding to ensure that the fields above don't share a
108+
// cache line with other allocations.
109+
char __padding[64];
110+
#endif
105111
} _PyThreadStateImpl;
106112

107113
#ifdef __cplusplus

Lib/test/test_dict.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1767,6 +1767,9 @@ def test_update(self):
17671767
self.assertEqual(copy, frozendict({'x': 1}))
17681768

17691769
def test_repr(self):
1770+
d = frozendict()
1771+
self.assertEqual(repr(d), "frozendict()")
1772+
17701773
d = frozendict(x=1, y=2)
17711774
self.assertEqual(repr(d), "frozendict({'x': 1, 'y': 2})")
17721775

@@ -1775,6 +1778,15 @@ class MyFrozenDict(frozendict):
17751778
d = MyFrozenDict(x=1, y=2)
17761779
self.assertEqual(repr(d), "MyFrozenDict({'x': 1, 'y': 2})")
17771780

1781+
def test_hash(self):
1782+
# hash() doesn't rely on the items order
1783+
self.assertEqual(hash(frozendict(x=1, y=2)),
1784+
hash(frozendict(y=2, x=1)))
1785+
1786+
fd = frozendict(x=[1], y=[2])
1787+
with self.assertRaisesRegex(TypeError, "unhashable type: 'list'"):
1788+
hash(fd)
1789+
17781790

17791791
if __name__ == "__main__":
17801792
unittest.main()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Align the QSBR thread state array to a 64-byte cache line boundary to
2+
avoid false sharing in the :term:`free-threaded build`.

Objects/dictobject.c

Lines changed: 65 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3381,19 +3381,18 @@ dict_dealloc(PyObject *self)
33813381

33823382

33833383
static PyObject *
3384-
dict_repr_lock_held(PyObject *self)
3384+
anydict_repr_impl(PyObject *self)
33853385
{
33863386
PyDictObject *mp = (PyDictObject *)self;
33873387
PyObject *key = NULL, *value = NULL;
3388-
ASSERT_DICT_LOCKED(mp);
33893388

3390-
int res = Py_ReprEnter((PyObject *)mp);
3389+
int res = Py_ReprEnter(self);
33913390
if (res != 0) {
33923391
return (res > 0 ? PyUnicode_FromString("{...}") : NULL);
33933392
}
33943393

33953394
if (mp->ma_used == 0) {
3396-
Py_ReprLeave((PyObject *)mp);
3395+
Py_ReprLeave(self);
33973396
return PyUnicode_FromString("{}");
33983397
}
33993398

@@ -3412,7 +3411,7 @@ dict_repr_lock_held(PyObject *self)
34123411
Note that repr may mutate the dict. */
34133412
Py_ssize_t i = 0;
34143413
int first = 1;
3415-
while (_PyDict_Next((PyObject *)mp, &i, &key, &value, NULL)) {
3414+
while (_PyDict_Next(self, &i, &key, &value, NULL)) {
34163415
// Prevent repr from deleting key or value during key format.
34173416
Py_INCREF(key);
34183417
Py_INCREF(value);
@@ -3454,18 +3453,25 @@ dict_repr_lock_held(PyObject *self)
34543453
goto error;
34553454
}
34563455

3457-
Py_ReprLeave((PyObject *)mp);
3456+
Py_ReprLeave(self);
34583457

34593458
return PyUnicodeWriter_Finish(writer);
34603459

34613460
error:
3462-
Py_ReprLeave((PyObject *)mp);
3461+
Py_ReprLeave(self);
34633462
PyUnicodeWriter_Discard(writer);
34643463
Py_XDECREF(key);
34653464
Py_XDECREF(value);
34663465
return NULL;
34673466
}
34683467

3468+
static PyObject *
3469+
dict_repr_lock_held(PyObject *self)
3470+
{
3471+
ASSERT_DICT_LOCKED((PyDictObject *)self);
3472+
return anydict_repr_impl(self);
3473+
}
3474+
34693475
static PyObject *
34703476
dict_repr(PyObject *self)
34713477
{
@@ -3482,6 +3488,12 @@ dict_length(PyObject *self)
34823488
return GET_USED(_PyAnyDict_CAST(self));
34833489
}
34843490

3491+
static Py_ssize_t
3492+
frozendict_length(PyObject *self)
3493+
{
3494+
return _PyAnyDict_CAST(self)->ma_used;
3495+
}
3496+
34853497
static PyObject *
34863498
dict_subscript(PyObject *self, PyObject *key)
34873499
{
@@ -7833,7 +7845,7 @@ static PyNumberMethods frozendict_as_number = {
78337845
};
78347846

78357847
static PyMappingMethods frozendict_as_mapping = {
7836-
.mp_length = dict_length,
7848+
.mp_length = frozendict_length,
78377849
.mp_subscript = dict_subscript,
78387850
};
78397851

@@ -7856,7 +7868,12 @@ static PyMethodDef frozendict_methods[] = {
78567868
static PyObject *
78577869
frozendict_repr(PyObject *self)
78587870
{
7859-
PyObject *repr = dict_repr(self);
7871+
PyDictObject *mp = _PyAnyDict_CAST(self);
7872+
if (mp->ma_used == 0) {
7873+
return PyUnicode_FromFormat("%s()", Py_TYPE(self)->tp_name);
7874+
}
7875+
7876+
PyObject *repr = anydict_repr_impl(self);
78607877
if (repr == NULL) {
78617878
return NULL;
78627879
}
@@ -7869,33 +7886,55 @@ frozendict_repr(PyObject *self)
78697886
return res;
78707887
}
78717888

7889+
static Py_uhash_t
7890+
_shuffle_bits(Py_uhash_t h)
7891+
{
7892+
return ((h ^ 89869747UL) ^ (h << 16)) * 3644798167UL;
7893+
}
7894+
7895+
// Code copied from frozenset_hash()
78727896
static Py_hash_t
78737897
frozendict_hash(PyObject *op)
78747898
{
78757899
PyFrozenDictObject *self = _PyFrozenDictObject_CAST(op);
7876-
Py_hash_t hash = FT_ATOMIC_LOAD_SSIZE_RELAXED(self->ma_hash);
7877-
if (hash != -1) {
7878-
return hash;
7900+
Py_hash_t shash = FT_ATOMIC_LOAD_SSIZE_RELAXED(self->ma_hash);
7901+
if (shash != -1) {
7902+
return shash;
78797903
}
78807904

7881-
PyObject *items = _PyDictView_New(op, &PyDictItems_Type);
7882-
if (items == NULL) {
7883-
return -1;
7884-
}
7885-
PyObject *frozenset = PyFrozenSet_New(items);
7886-
Py_DECREF(items);
7887-
if (frozenset == NULL) {
7888-
return -1;
7905+
PyDictObject *mp = _PyAnyDict_CAST(op);
7906+
Py_uhash_t hash = 0;
7907+
7908+
PyObject *key, *value; // borrowed refs
7909+
Py_ssize_t pos = 0;
7910+
while (PyDict_Next(op, &pos, &key, &value)) {
7911+
Py_hash_t key_hash = PyObject_Hash(key);
7912+
if (key_hash == -1) {
7913+
return -1;
7914+
}
7915+
hash ^= _shuffle_bits(key_hash);
7916+
7917+
Py_hash_t value_hash = PyObject_Hash(value);
7918+
if (value_hash == -1) {
7919+
return -1;
7920+
}
7921+
hash ^= _shuffle_bits(value_hash);
78897922
}
78907923

7891-
hash = PyObject_Hash(frozenset);
7892-
Py_DECREF(frozenset);
7893-
if (hash == -1) {
7894-
return -1;
7924+
/* Factor in the number of active entries */
7925+
hash ^= ((Py_uhash_t)mp->ma_used + 1) * 1927868237UL;
7926+
7927+
/* Disperse patterns arising in nested frozendicts */
7928+
hash ^= (hash >> 11) ^ (hash >> 25);
7929+
hash = hash * 69069U + 907133923UL;
7930+
7931+
/* -1 is reserved as an error code */
7932+
if (hash == (Py_uhash_t)-1) {
7933+
hash = 590923713UL;
78957934
}
78967935

7897-
FT_ATOMIC_STORE_SSIZE_RELAXED(self->ma_hash, hash);
7898-
return hash;
7936+
FT_ATOMIC_STORE_SSIZE_RELAXED(self->ma_hash, (Py_hash_t)hash);
7937+
return (Py_hash_t)hash;
78997938
}
79007939

79017940

Objects/setobject.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -964,7 +964,10 @@ _shuffle_bits(Py_uhash_t h)
964964
965965
This hash algorithm can be used on either a frozenset or a set.
966966
When it is used on a set, it computes the hash value of the equivalent
967-
frozenset without creating a new frozenset object. */
967+
frozenset without creating a new frozenset object.
968+
969+
If you update this code, update also frozendict_hash() which copied this
970+
code. */
968971

969972
static Py_hash_t
970973
frozenset_hash_impl(PyObject *self)

Python/qsbr.c

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,22 +85,29 @@ grow_thread_array(struct _qsbr_shared *shared)
8585
new_size = MIN_ARRAY_SIZE;
8686
}
8787

88-
struct _qsbr_pad *array = PyMem_RawCalloc(new_size, sizeof(*array));
89-
if (array == NULL) {
88+
// Overallocate by 63 bytes so we can align to a 64-byte boundary.
89+
// This avoids potential false sharing between the first entry and other
90+
// allocations.
91+
size_t alignment = 64;
92+
size_t alloc_size = (size_t)new_size * sizeof(struct _qsbr_pad) + alignment - 1;
93+
void *raw = PyMem_RawCalloc(1, alloc_size);
94+
if (raw == NULL) {
9095
return -1;
9196
}
97+
struct _qsbr_pad *array = _Py_ALIGN_UP(raw, alignment);
9298

93-
struct _qsbr_pad *old = shared->array;
94-
if (old != NULL) {
99+
void *old_raw = shared->array_raw;
100+
if (shared->array != NULL) {
95101
memcpy(array, shared->array, shared->size * sizeof(*array));
96102
}
97103

98104
shared->array = array;
105+
shared->array_raw = raw;
99106
shared->size = new_size;
100107
shared->freelist = NULL;
101108
initialize_new_array(shared);
102109

103-
PyMem_RawFree(old);
110+
PyMem_RawFree(old_raw);
104111
return 0;
105112
}
106113

@@ -257,8 +264,9 @@ void
257264
_Py_qsbr_fini(PyInterpreterState *interp)
258265
{
259266
struct _qsbr_shared *shared = &interp->qsbr;
260-
PyMem_RawFree(shared->array);
267+
PyMem_RawFree(shared->array_raw);
261268
shared->array = NULL;
269+
shared->array_raw = NULL;
262270
shared->size = 0;
263271
shared->freelist = NULL;
264272
}

0 commit comments

Comments
 (0)