Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 7ccacb2

Browse filesBrowse files
authored
gh-117783: Immortalize objects that use deferred reference counting (#118112)
Deferred reference counting is not fully implemented yet. As a temporary measure, we immortalize objects that would use deferred reference counting to avoid multi-threaded scaling bottlenecks. This is only performed in the free-threaded build once the first non-main thread is started. Additionally, some tests, including refleak tests, suppress this behavior.
1 parent 8d4b756 commit 7ccacb2
Copy full SHA for 7ccacb2

File tree

Expand file treeCollapse file tree

13 files changed

+134
-8
lines changed
Filter options
Expand file treeCollapse file tree

13 files changed

+134
-8
lines changed

‎Include/internal/pycore_gc.h

Copy file name to clipboardExpand all lines: Include/internal/pycore_gc.h
+17Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,18 @@ struct _gc_runtime_state {
312312
collections, and are awaiting to undergo a full collection for
313313
the first time. */
314314
Py_ssize_t long_lived_pending;
315+
316+
/* gh-117783: Deferred reference counting is not fully implemented yet, so
317+
as a temporary measure we treat objects using deferred referenence
318+
counting as immortal. */
319+
struct {
320+
/* Immortalize objects instead of marking them as using deferred
321+
reference counting. */
322+
int enabled;
323+
324+
/* Set enabled=1 when the first background thread is created. */
325+
int enable_on_thread_created;
326+
} immortalize;
315327
#endif
316328
};
317329

@@ -343,6 +355,11 @@ extern void _PyGC_ClearAllFreeLists(PyInterpreterState *interp);
343355
extern void _Py_ScheduleGC(PyThreadState *tstate);
344356
extern void _Py_RunGC(PyThreadState *tstate);
345357

358+
#ifdef Py_GIL_DISABLED
359+
// gh-117783: Immortalize objects that use deferred reference counting
360+
extern void _PyGC_ImmortalizeDeferredObjects(PyInterpreterState *interp);
361+
#endif
362+
346363
#ifdef __cplusplus
347364
}
348365
#endif

‎Lib/concurrent/futures/process.py

Copy file name to clipboardExpand all lines: Lib/concurrent/futures/process.py
+3-2Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -296,8 +296,9 @@ def __init__(self, executor):
296296
# if there is no pending work item.
297297
def weakref_cb(_,
298298
thread_wakeup=self.thread_wakeup,
299-
shutdown_lock=self.shutdown_lock):
300-
mp.util.debug('Executor collected: triggering callback for'
299+
shutdown_lock=self.shutdown_lock,
300+
mp_util_debug=mp.util.debug):
301+
mp_util_debug('Executor collected: triggering callback for'
301302
' QueueManager wakeup')
302303
with shutdown_lock:
303304
thread_wakeup.wakeup()

‎Lib/test/libregrtest/main.py

Copy file name to clipboardExpand all lines: Lib/test/libregrtest/main.py
+6-2Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
import time
88
import trace
99

10-
from test.support import os_helper, MS_WINDOWS, flush_std_streams
10+
from test.support import (os_helper, MS_WINDOWS, flush_std_streams,
11+
suppress_immortalization)
1112

1213
from .cmdline import _parse_args, Namespace
1314
from .findtests import findtests, split_test_packages, list_cases
@@ -526,7 +527,10 @@ def _run_tests(self, selected: TestTuple, tests: TestList | None) -> int:
526527
if self.num_workers:
527528
self._run_tests_mp(runtests, self.num_workers)
528529
else:
529-
self.run_tests_sequentially(runtests)
530+
# gh-117783: don't immortalize deferred objects when tracking
531+
# refleaks. Only releveant for the free-threaded build.
532+
with suppress_immortalization(runtests.hunt_refleak):
533+
self.run_tests_sequentially(runtests)
530534

531535
coverage = self.results.get_coverage_results()
532536
self.display_result(runtests)

‎Lib/test/libregrtest/single.py

Copy file name to clipboardExpand all lines: Lib/test/libregrtest/single.py
+4-1Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,10 @@ def run_single_test(test_name: TestName, runtests: RunTests) -> TestResult:
303303
result = TestResult(test_name)
304304
pgo = runtests.pgo
305305
try:
306-
_runtest(result, runtests)
306+
# gh-117783: don't immortalize deferred objects when tracking
307+
# refleaks. Only releveant for the free-threaded build.
308+
with support.suppress_immortalization(runtests.hunt_refleak):
309+
_runtest(result, runtests)
307310
except:
308311
if not pgo:
309312
msg = traceback.format_exc()

‎Lib/test/support/__init__.py

Copy file name to clipboardExpand all lines: Lib/test/support/__init__.py
+19Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,25 @@ def has_no_debug_ranges():
516516
def requires_debug_ranges(reason='requires co_positions / debug_ranges'):
517517
return unittest.skipIf(has_no_debug_ranges(), reason)
518518

519+
@contextlib.contextmanager
520+
def suppress_immortalization(suppress=True):
521+
"""Suppress immortalization of deferred objects."""
522+
try:
523+
import _testinternalcapi
524+
except ImportError:
525+
yield
526+
return
527+
528+
if not suppress:
529+
yield
530+
return
531+
532+
old_values = _testinternalcapi.set_immortalize_deferred(False)
533+
try:
534+
yield
535+
finally:
536+
_testinternalcapi.set_immortalize_deferred(*old_values)
537+
519538
MS_WINDOWS = (sys.platform == 'win32')
520539

521540
# Is not actually used in tests, but is kept for compatibility.

‎Lib/test/test_capi/test_watchers.py

Copy file name to clipboardExpand all lines: Lib/test/test_capi/test_watchers.py
+5-1Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import unittest
22

33
from contextlib import contextmanager, ExitStack
4-
from test.support import catch_unraisable_exception, import_helper, gc_collect
4+
from test.support import (
5+
catch_unraisable_exception, import_helper,
6+
gc_collect, suppress_immortalization)
57

68

79
# Skip this test if the _testcapi module isn't available.
@@ -382,6 +384,7 @@ def assert_event_counts(self, exp_created_0, exp_destroyed_0,
382384
self.assertEqual(
383385
exp_destroyed_1, _testcapi.get_code_watcher_num_destroyed_events(1))
384386

387+
@suppress_immortalization()
385388
def test_code_object_events_dispatched(self):
386389
# verify that all counts are zero before any watchers are registered
387390
self.assert_event_counts(0, 0, 0, 0)
@@ -428,6 +431,7 @@ def test_error(self):
428431
self.assertIsNone(cm.unraisable.object)
429432
self.assertEqual(str(cm.unraisable.exc_value), "boom!")
430433

434+
@suppress_immortalization()
431435
def test_dealloc_error(self):
432436
co = _testcapi.code_newempty("test_watchers", "dummy0", 0)
433437
with self.code_watcher(2):

‎Lib/test/test_code.py

Copy file name to clipboardExpand all lines: Lib/test/test_code.py
+5-1Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,8 @@
141141
ctypes = None
142142
from test.support import (cpython_only,
143143
check_impl_detail, requires_debug_ranges,
144-
gc_collect, Py_GIL_DISABLED)
144+
gc_collect, Py_GIL_DISABLED,
145+
suppress_immortalization)
145146
from test.support.script_helper import assert_python_ok
146147
from test.support import threading_helper, import_helper
147148
from test.support.bytecode_helper import instructions_with_positions
@@ -577,6 +578,7 @@ def test_interned_string_with_null(self):
577578

578579
class CodeWeakRefTest(unittest.TestCase):
579580

581+
@suppress_immortalization()
580582
def test_basic(self):
581583
# Create a code object in a clean environment so that we know we have
582584
# the only reference to it left.
@@ -827,6 +829,7 @@ def test_bad_index(self):
827829
self.assertEqual(GetExtra(f.__code__, FREE_INDEX+100,
828830
ctypes.c_voidp(100)), 0)
829831

832+
@suppress_immortalization()
830833
def test_free_called(self):
831834
# Verify that the provided free function gets invoked
832835
# when the code object is cleaned up.
@@ -854,6 +857,7 @@ def test_get_set(self):
854857
del f
855858

856859
@threading_helper.requires_working_threading()
860+
@suppress_immortalization()
857861
def test_free_different_thread(self):
858862
# Freeing a code object on a different thread then
859863
# where the co_extra was set should be safe.

‎Lib/test/test_functools.py

Copy file name to clipboardExpand all lines: Lib/test/test_functools.py
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1833,6 +1833,7 @@ def f():
18331833
return 1
18341834
self.assertEqual(f.cache_parameters(), {'maxsize': 1000, "typed": True})
18351835

1836+
@support.suppress_immortalization()
18361837
def test_lru_cache_weakrefable(self):
18371838
@self.module.lru_cache
18381839
def test_function(x):

‎Lib/test/test_weakref.py

Copy file name to clipboardExpand all lines: Lib/test/test_weakref.py
+4-1Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import textwrap
1414

1515
from test import support
16-
from test.support import script_helper, ALWAYS_EQ
16+
from test.support import script_helper, ALWAYS_EQ, suppress_immortalization
1717
from test.support import gc_collect
1818
from test.support import import_helper
1919
from test.support import threading_helper
@@ -651,6 +651,7 @@ class C(object):
651651
# deallocation of c2.
652652
del c2
653653

654+
@suppress_immortalization()
654655
def test_callback_in_cycle(self):
655656
import gc
656657

@@ -743,6 +744,7 @@ class D:
743744
del c1, c2, C, D
744745
gc.collect()
745746

747+
@suppress_immortalization()
746748
def test_callback_in_cycle_resurrection(self):
747749
import gc
748750

@@ -878,6 +880,7 @@ def test_init(self):
878880
# No exception should be raised here
879881
gc.collect()
880882

883+
@suppress_immortalization()
881884
def test_classes(self):
882885
# Check that classes are weakrefable.
883886
class A(object):

‎Modules/_testinternalcapi.c

Copy file name to clipboardExpand all lines: Modules/_testinternalcapi.c
+22Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1957,6 +1957,27 @@ get_py_thread_id(PyObject *self, PyObject *Py_UNUSED(ignored))
19571957
}
19581958
#endif
19591959

1960+
static PyObject *
1961+
set_immortalize_deferred(PyObject *self, PyObject *value)
1962+
{
1963+
#ifdef Py_GIL_DISABLED
1964+
PyInterpreterState *interp = PyInterpreterState_Get();
1965+
int old_enabled = interp->gc.immortalize.enabled;
1966+
int old_enabled_on_thread = interp->gc.immortalize.enable_on_thread_created;
1967+
int enabled_on_thread = 0;
1968+
if (!PyArg_ParseTuple(value, "i|i",
1969+
&interp->gc.immortalize.enabled,
1970+
&enabled_on_thread))
1971+
{
1972+
return NULL;
1973+
}
1974+
interp->gc.immortalize.enable_on_thread_created = enabled_on_thread;
1975+
return Py_BuildValue("ii", old_enabled, old_enabled_on_thread);
1976+
#else
1977+
return Py_BuildValue("OO", Py_False, Py_False);
1978+
#endif
1979+
}
1980+
19601981
static PyObject *
19611982
has_inline_values(PyObject *self, PyObject *obj)
19621983
{
@@ -2050,6 +2071,7 @@ static PyMethodDef module_functions[] = {
20502071
#ifdef Py_GIL_DISABLED
20512072
{"py_thread_id", get_py_thread_id, METH_NOARGS},
20522073
#endif
2074+
{"set_immortalize_deferred", set_immortalize_deferred, METH_VARARGS},
20532075
{"uop_symbols_test", _Py_uop_symbols_test, METH_NOARGS},
20542076
{NULL, NULL} /* sentinel */
20552077
};

‎Objects/object.c

Copy file name to clipboardExpand all lines: Objects/object.c
+7Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2430,6 +2430,13 @@ _PyObject_SetDeferredRefcount(PyObject *op)
24302430
assert(PyType_IS_GC(Py_TYPE(op)));
24312431
assert(_Py_IsOwnedByCurrentThread(op));
24322432
assert(op->ob_ref_shared == 0);
2433+
PyInterpreterState *interp = _PyInterpreterState_GET();
2434+
if (interp->gc.immortalize.enabled) {
2435+
// gh-117696: immortalize objects instead of using deferred reference
2436+
// counting for now.
2437+
_Py_SetImmortal(op);
2438+
return;
2439+
}
24332440
op->ob_gc_bits |= _PyGC_BITS_DEFERRED;
24342441
op->ob_ref_local += 1;
24352442
op->ob_ref_shared = _Py_REF_QUEUED;

‎Python/gc_free_threading.c

Copy file name to clipboardExpand all lines: Python/gc_free_threading.c
+30Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,12 @@ _PyGC_Init(PyInterpreterState *interp)
704704
{
705705
GCState *gcstate = &interp->gc;
706706

707+
if (_Py_IsMainInterpreter(interp)) {
708+
// gh-117783: immortalize objects that would use deferred refcounting
709+
// once the first non-main thread is created.
710+
gcstate->immortalize.enable_on_thread_created = 1;
711+
}
712+
707713
gcstate->garbage = PyList_New(0);
708714
if (gcstate->garbage == NULL) {
709715
return _PyStatus_NO_MEMORY();
@@ -1781,6 +1787,30 @@ custom_visitor_wrapper(const mi_heap_t *heap, const mi_heap_area_t *area,
17811787
return true;
17821788
}
17831789

1790+
// gh-117783: Immortalize objects that use deferred reference counting to
1791+
// temporarily work around scaling bottlenecks.
1792+
static bool
1793+
immortalize_visitor(const mi_heap_t *heap, const mi_heap_area_t *area,
1794+
void *block, size_t block_size, void *args)
1795+
{
1796+
PyObject *op = op_from_block(block, args, false);
1797+
if (op != NULL && _PyObject_HasDeferredRefcount(op)) {
1798+
_Py_SetImmortal(op);
1799+
op->ob_gc_bits &= ~_PyGC_BITS_DEFERRED;
1800+
}
1801+
return true;
1802+
}
1803+
1804+
void
1805+
_PyGC_ImmortalizeDeferredObjects(PyInterpreterState *interp)
1806+
{
1807+
struct visitor_args args;
1808+
_PyEval_StopTheWorld(interp);
1809+
gc_visit_heaps(interp, &immortalize_visitor, &args);
1810+
interp->gc.immortalize.enabled = 1;
1811+
_PyEval_StartTheWorld(interp);
1812+
}
1813+
17841814
void
17851815
PyUnstable_GC_VisitObjects(gcvisitobjects_t callback, void *arg)
17861816
{

‎Python/pystate.c

Copy file name to clipboardExpand all lines: Python/pystate.c
+11Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1568,6 +1568,17 @@ new_threadstate(PyInterpreterState *interp, int whence)
15681568
// Must be called with lock unlocked to avoid re-entrancy deadlock.
15691569
PyMem_RawFree(new_tstate);
15701570
}
1571+
else {
1572+
#ifdef Py_GIL_DISABLED
1573+
if (interp->gc.immortalize.enable_on_thread_created &&
1574+
!interp->gc.immortalize.enabled)
1575+
{
1576+
// Immortalize objects marked as using deferred reference counting
1577+
// the first time a non-main thread is created.
1578+
_PyGC_ImmortalizeDeferredObjects(interp);
1579+
}
1580+
#endif
1581+
}
15711582

15721583
#ifdef Py_GIL_DISABLED
15731584
// Must be called with lock unlocked to avoid lock ordering deadlocks.

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.