Skip to content

Commit 3d4ac1a

Browse files
colesburycorona10
andauthored
pythongh-123358: Use _PyStackRef in LOAD_DEREF (pythongh-130064)
Concurrent accesses from multiple threads to the same `cell` object did not scale well in the free-threaded build. Use `_PyStackRef` and optimistically avoid locking to improve scaling. With the locks around cell reads gone, some of the free threading tests were prone to starvation: the readers were able to run in a tight loop and the writer threads weren't scheduled frequently enough to make timely progress. Adjust the tests to avoid this. Co-authored-by: Donghee Na <donghee.na@python.org>
1 parent 1b8bb1e commit 3d4ac1a

File tree

12 files changed

+90
-44
lines changed

12 files changed

+90
-44
lines changed

Include/internal/pycore_cell.h

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
#define Py_INTERNAL_CELL_H
33

44
#include "pycore_critical_section.h"
5+
#include "pycore_object.h"
6+
#include "pycore_stackref.h"
57

68
#ifdef __cplusplus
79
extern "C" {
@@ -19,7 +21,7 @@ PyCell_SwapTakeRef(PyCellObject *cell, PyObject *value)
1921
PyObject *old_value;
2022
Py_BEGIN_CRITICAL_SECTION(cell);
2123
old_value = cell->ob_ref;
22-
cell->ob_ref = value;
24+
FT_ATOMIC_STORE_PTR_RELEASE(cell->ob_ref, value);
2325
Py_END_CRITICAL_SECTION();
2426
return old_value;
2527
}
@@ -37,11 +39,36 @@ PyCell_GetRef(PyCellObject *cell)
3739
{
3840
PyObject *res;
3941
Py_BEGIN_CRITICAL_SECTION(cell);
42+
#ifdef Py_GIL_DISABLED
43+
res = _Py_XNewRefWithLock(cell->ob_ref);
44+
#else
4045
res = Py_XNewRef(cell->ob_ref);
46+
#endif
4147
Py_END_CRITICAL_SECTION();
4248
return res;
4349
}
4450

51+
static inline _PyStackRef
52+
_PyCell_GetStackRef(PyCellObject *cell)
53+
{
54+
PyObject *value;
55+
#ifdef Py_GIL_DISABLED
56+
value = _Py_atomic_load_ptr(&cell->ob_ref);
57+
if (value == NULL) {
58+
return PyStackRef_NULL;
59+
}
60+
_PyStackRef ref;
61+
if (_Py_TryIncrefCompareStackRef(&cell->ob_ref, value, &ref)) {
62+
return ref;
63+
}
64+
#endif
65+
value = PyCell_GetRef(cell);
66+
if (value == NULL) {
67+
return PyStackRef_NULL;
68+
}
69+
return PyStackRef_FromPyObjectSteal(value);
70+
}
71+
4572
#ifdef __cplusplus
4673
}
4774
#endif

Include/internal/pycore_opcode_metadata.h

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_uop_metadata.h

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/test/test_free_threading/test_dict.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ def writer_func(name):
7474
last = -1
7575
while True:
7676
if CUR == last:
77+
time.sleep(0.001)
7778
continue
7879
elif CUR == OBJECT_COUNT:
7980
break

Lib/test/test_free_threading/test_func_annotations.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,13 @@ def set_func_annotation(f, b):
2727

2828
@unittest.skipUnless(Py_GIL_DISABLED, "Enable only in FT build")
2929
class TestFTFuncAnnotations(TestCase):
30-
NUM_THREADS = 8
30+
NUM_THREADS = 4
3131

3232
def test_concurrent_read(self):
3333
def f(x: int) -> int:
3434
return x + 1
3535

36-
for _ in range(100):
36+
for _ in range(10):
3737
with concurrent.futures.ThreadPoolExecutor(max_workers=self.NUM_THREADS) as executor:
3838
b = Barrier(self.NUM_THREADS)
3939
futures = {executor.submit(get_func_annotation, f, b): i for i in range(self.NUM_THREADS)}
@@ -54,7 +54,7 @@ def test_concurrent_write(self):
5454
def bar(x: int, y: float) -> float:
5555
return y ** x
5656

57-
for _ in range(100):
57+
for _ in range(10):
5858
with concurrent.futures.ThreadPoolExecutor(max_workers=self.NUM_THREADS) as executor:
5959
b = Barrier(self.NUM_THREADS)
6060
futures = {executor.submit(set_func_annotation, bar, b): i for i in range(self.NUM_THREADS)}

Lib/test/test_free_threading/test_gc.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,24 +35,30 @@ def mutator_thread():
3535
pass
3636

3737
def test_get_referrers(self):
38+
NUM_GC = 2
39+
NUM_MUTATORS = 4
40+
41+
b = threading.Barrier(NUM_GC + NUM_MUTATORS)
3842
event = threading.Event()
3943

4044
obj = MyObj()
4145

4246
def gc_thread():
47+
b.wait()
4348
for i in range(100):
4449
o = gc.get_referrers(obj)
4550
event.set()
4651

4752
def mutator_thread():
53+
b.wait()
4854
while not event.is_set():
4955
d1 = { "key": obj }
5056
d2 = { "key": obj }
5157
d3 = { "key": obj }
5258
d4 = { "key": obj }
5359

54-
gcs = [Thread(target=gc_thread) for _ in range(2)]
55-
mutators = [Thread(target=mutator_thread) for _ in range(4)]
60+
gcs = [Thread(target=gc_thread) for _ in range(NUM_GC)]
61+
mutators = [Thread(target=mutator_thread) for _ in range(NUM_MUTATORS)]
5662
with threading_helper.start_threads(gcs + mutators):
5763
pass
5864

Lib/test/test_free_threading/test_list.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,22 +20,25 @@ class TestList(TestCase):
2020
def test_racing_iter_append(self):
2121
l = []
2222

23-
def writer_func():
23+
barrier = Barrier(NTHREAD + 1)
24+
def writer_func(l):
25+
barrier.wait()
2426
for i in range(OBJECT_COUNT):
2527
l.append(C(i + OBJECT_COUNT))
2628

27-
def reader_func():
29+
def reader_func(l):
30+
barrier.wait()
2831
while True:
2932
count = len(l)
3033
for i, x in enumerate(l):
3134
self.assertEqual(x.v, i + OBJECT_COUNT)
3235
if count == OBJECT_COUNT:
3336
break
3437

35-
writer = Thread(target=writer_func)
38+
writer = Thread(target=writer_func, args=(l,))
3639
readers = []
3740
for x in range(NTHREAD):
38-
reader = Thread(target=reader_func)
41+
reader = Thread(target=reader_func, args=(l,))
3942
readers.append(reader)
4043
reader.start()
4144

@@ -47,11 +50,14 @@ def reader_func():
4750
def test_racing_iter_extend(self):
4851
l = []
4952

53+
barrier = Barrier(NTHREAD + 1)
5054
def writer_func():
55+
barrier.wait()
5156
for i in range(OBJECT_COUNT):
5257
l.extend([C(i + OBJECT_COUNT)])
5358

5459
def reader_func():
60+
barrier.wait()
5561
while True:
5662
count = len(l)
5763
for i, x in enumerate(l):

Lib/test/test_free_threading/test_monitoring.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from sys import monitoring
1010
from test.support import threading_helper
11-
from threading import Thread, _PyRLock
11+
from threading import Thread, _PyRLock, Barrier
1212
from unittest import TestCase
1313

1414

@@ -194,7 +194,9 @@ def during_threads(self):
194194

195195
@threading_helper.requires_working_threading()
196196
class MonitoringMisc(MonitoringTestMixin, TestCase):
197-
def register_callback(self):
197+
def register_callback(self, barrier):
198+
barrier.wait()
199+
198200
def callback(*args):
199201
pass
200202

@@ -206,8 +208,9 @@ def callback(*args):
206208
def test_register_callback(self):
207209
self.refs = []
208210
threads = []
209-
for i in range(50):
210-
t = Thread(target=self.register_callback)
211+
barrier = Barrier(5)
212+
for i in range(5):
213+
t = Thread(target=self.register_callback, args=(barrier,))
211214
t.start()
212215
threads.append(t)
213216

Lib/test/test_free_threading/test_type.py

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -45,26 +45,20 @@ def test_attr_cache_consistency(self):
4545
class C:
4646
x = 0
4747

48-
DONE = False
4948
def writer_func():
50-
for i in range(3000):
49+
for _ in range(3000):
5150
C.x
5251
C.x
5352
C.x += 1
54-
nonlocal DONE
55-
DONE = True
5653

5754
def reader_func():
58-
while True:
55+
for _ in range(3000):
5956
# We should always see a greater value read from the type than the
6057
# dictionary
6158
a = C.__dict__['x']
6259
b = C.x
6360
self.assertGreaterEqual(b, a)
6461

65-
if DONE:
66-
break
67-
6862
self.run_one(writer_func, reader_func)
6963

7064
def test_attr_cache_consistency_subclass(self):
@@ -74,26 +68,20 @@ class C:
7468
class D(C):
7569
pass
7670

77-
DONE = False
7871
def writer_func():
79-
for i in range(3000):
72+
for _ in range(3000):
8073
D.x
8174
D.x
8275
C.x += 1
83-
nonlocal DONE
84-
DONE = True
8576

8677
def reader_func():
87-
while True:
78+
for _ in range(3000):
8879
# We should always see a greater value read from the type than the
8980
# dictionary
9081
a = C.__dict__['x']
9182
b = D.x
9283
self.assertGreaterEqual(b, a)
9384

94-
if DONE:
95-
break
96-
9785
self.run_one(writer_func, reader_func)
9886

9987
def test___class___modification(self):
@@ -140,10 +128,18 @@ class ClassB(Base):
140128

141129

142130
def run_one(self, writer_func, reader_func):
143-
writer = Thread(target=writer_func)
131+
barrier = threading.Barrier(NTHREADS)
132+
133+
def wrap_target(target):
134+
def wrapper():
135+
barrier.wait()
136+
target()
137+
return wrapper
138+
139+
writer = Thread(target=wrap_target(writer_func))
144140
readers = []
145-
for x in range(30):
146-
reader = Thread(target=reader_func)
141+
for x in range(NTHREADS - 1):
142+
reader = Thread(target=wrap_target(reader_func))
147143
readers.append(reader)
148144
reader.start()
149145

Python/bytecodes.c

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1822,12 +1822,11 @@ dummy_func(
18221822

18231823
inst(LOAD_DEREF, ( -- value)) {
18241824
PyCellObject *cell = (PyCellObject *)PyStackRef_AsPyObjectBorrow(GETLOCAL(oparg));
1825-
PyObject *value_o = PyCell_GetRef(cell);
1826-
if (value_o == NULL) {
1825+
value = _PyCell_GetStackRef(cell);
1826+
if (PyStackRef_IsNull(value)) {
18271827
_PyEval_FormatExcUnbound(tstate, _PyFrame_GetCode(frame), oparg);
18281828
ERROR_IF(true, error);
18291829
}
1830-
value = PyStackRef_FromPyObjectSteal(value_o);
18311830
}
18321831

18331832
inst(STORE_DEREF, (v --)) {

Python/executor_cases.c.h

Lines changed: 7 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Python/generated_cases.c.h

Lines changed: 7 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)