Skip to content

Commit 69d37ec

Browse files
gh-155725: Remove PyGILState_Ensure from tracemalloc
tracemalloc no longer acquires the GIL nor creates a temporary thread state when tracing memory allocations. A thread with no attached thread state now records the trace with the "<unknown>" traceback instead of attaching a thread state to capture the Python traceback. Threads without a thread state used to pay for a GIL acquisition plus a full thread state creation and destruction on every traced raw allocation, only to record an empty traceback anyway.
1 parent e2118b0 commit 69d37ec

4 files changed

Lines changed: 53 additions & 79 deletions

File tree

Doc/c-api/memory.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,17 @@ tracemalloc C API
780780
781781
If memory block is already tracked, update the existing trace.
782782
783+
The function can be called from any thread, with or without an
784+
:term:`attached thread state`. If the calling thread has no attached
785+
thread state, the traceback of the allocation is recorded as
786+
``<unknown>``.
787+
788+
.. versionchanged:: next
789+
The function no longer acquires the :term:`GIL`. When called from a
790+
thread without an :term:`attached thread state`, the traceback is now
791+
recorded as ``<unknown>`` instead of the Python traceback of the
792+
calling thread.
793+
783794
.. c:function:: int PyTraceMalloc_Untrack(unsigned int domain, uintptr_t ptr)
784795
785796
Untrack an allocated memory block in the :mod:`tracemalloc` module.

Lib/test/test_tracemalloc.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1047,17 +1047,22 @@ def check_track(self, release_gil):
10471047
size = tracemalloc.get_traced_memory()[0]
10481048

10491049
frames = self.track(release_gil, nframe)
1050-
self.assertEqual(self.get_traceback(),
1051-
tracemalloc.Traceback(frames))
1050+
if release_gil:
1051+
# PyTraceMalloc_Track() never acquires the GIL: without an
1052+
# attached thread state, the traceback is recorded as <unknown>
1053+
expected = tracemalloc.Traceback([("<unknown>", 0)])
1054+
else:
1055+
expected = tracemalloc.Traceback(frames)
1056+
self.assertEqual(self.get_traceback(), expected)
10521057

10531058
self.assertEqual(self.get_traced_memory(), self.size)
10541059

10551060
def test_track(self):
10561061
self.check_track(False)
10571062

10581063
def test_track_without_gil(self):
1059-
# check that calling _PyTraceMalloc_Track() without holding the GIL
1060-
# works too
1064+
# check that calling PyTraceMalloc_Track() with the thread state
1065+
# detached (GIL released) records the trace with an unknown traceback
10611066
self.check_track(True)
10621067

10631068
def test_track_already_tracked(self):
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
:mod:`tracemalloc` no longer acquires the :term:`GIL` nor creates a
2+
temporary thread state when tracing memory allocations. Memory allocations
3+
and :c:func:`PyTraceMalloc_Track` calls made by threads without an
4+
:term:`attached thread state` now record the traceback as ``<unknown>``
5+
instead of temporarily attaching a thread state to capture the Python
6+
traceback of the calling thread.

Python/tracemalloc.c

Lines changed: 27 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,10 @@ static int _PyTraceMalloc_TraceRef(PyObject *op, PyRefTracerEvent event,
3232
#define allocators _PyRuntime.tracemalloc.allocators
3333

3434

35-
/* This lock is needed because tracemalloc_free() is called without
36-
the GIL held from PyMem_RawFree(). It cannot acquire the lock because it
37-
would introduce a deadlock in _PyThreadState_DeleteCurrent(). */
35+
/* This lock protects the trace tables. It is acquired by threads which may
36+
not have an attached thread state, such as tracemalloc_free() called from
37+
PyMem_RawFree(): tracing never acquires the GIL nor attaches a thread
38+
state. */
3839
#define tables_lock _PyRuntime.tracemalloc.tables_lock
3940
#define TABLES_LOCK() PyMutex_LockFlags(&tables_lock, _Py_LOCK_DONT_DETACH)
4041
#define TABLES_UNLOCK() PyMutex_Unlock(&tables_lock)
@@ -303,11 +304,8 @@ traceback_hash(traceback_t *traceback)
303304

304305

305306
static void
306-
traceback_get_frames(traceback_t *traceback)
307+
traceback_get_frames(traceback_t *traceback, PyThreadState *tstate)
307308
{
308-
PyThreadState *tstate = _PyThreadState_GET();
309-
assert(tstate != NULL);
310-
311309
_PyInterpreterFrame *pyframe = _PyThreadState_GetFrame(tstate);
312310
while (pyframe) {
313311
if (traceback->nframe < tracemalloc_config.max_nframe) {
@@ -329,13 +327,19 @@ traceback_new(void)
329327
traceback_t *traceback;
330328
_Py_hashtable_entry_t *entry;
331329

332-
_Py_AssertHoldsTstate();
330+
// A thread with no attached thread state cannot capture a Python
331+
// traceback and must not use Python objects, such as the interned
332+
// filenames: record the trace with the "<unknown>" traceback instead.
333+
PyThreadState *tstate = _PyThreadState_GET();
334+
if (tstate == NULL) {
335+
return tracemalloc_empty_traceback;
336+
}
333337

334338
/* get frames */
335339
traceback = tracemalloc_traceback;
336340
traceback->nframe = 0;
337341
traceback->total_nframe = 0;
338-
traceback_get_frames(traceback);
342+
traceback_get_frames(traceback, tstate);
339343
if (traceback->nframe == 0) {
340344
return tracemalloc_empty_traceback;
341345
}
@@ -497,21 +501,17 @@ tracemalloc_add_trace_unlocked(unsigned int domain, uintptr_t ptr,
497501

498502

499503
static void*
500-
tracemalloc_alloc(int need_gil, int use_calloc,
501-
void *ctx, size_t nelem, size_t elsize)
504+
tracemalloc_alloc(int use_calloc, void *ctx, size_t nelem, size_t elsize)
502505
{
503506
assert(elsize == 0 || nelem <= SIZE_MAX / elsize);
504507

505508
int reentrant = get_reentrant();
506509

507510
// Ignore reentrant call.
508511
//
509-
// For example, PyObjet_Malloc() calls
512+
// For example, PyObject_Malloc() calls
510513
// PyMem_Malloc() for allocations larger than 512 bytes: don't trace the
511514
// same memory allocation twice.
512-
//
513-
// If reentrant calls are not ignored, PyGILState_Ensure() can call
514-
// PyMem_RawMalloc() which would call PyGILState_Ensure() again in a loop.
515515
if (!reentrant) {
516516
set_reentrant(1);
517517
}
@@ -532,10 +532,6 @@ tracemalloc_alloc(int need_gil, int use_calloc,
532532
goto done;
533533
}
534534

535-
PyGILState_STATE gil_state;
536-
if (need_gil) {
537-
gil_state = PyGILState_Ensure();
538-
}
539535
TABLES_LOCK();
540536

541537
if (tracemalloc_config.tracing) {
@@ -548,9 +544,6 @@ tracemalloc_alloc(int need_gil, int use_calloc,
548544
// else: gh-128679: tracemalloc.stop() was called by another thread
549545

550546
TABLES_UNLOCK();
551-
if (need_gil) {
552-
PyGILState_Release(gil_state);
553-
}
554547

555548
done:
556549
if (!reentrant) {
@@ -561,7 +554,7 @@ tracemalloc_alloc(int need_gil, int use_calloc,
561554

562555

563556
static void*
564-
tracemalloc_realloc(int need_gil, void *ctx, void *ptr, size_t new_size)
557+
tracemalloc_realloc(void *ctx, void *ptr, size_t new_size)
565558
{
566559
int reentrant = get_reentrant();
567560

@@ -582,10 +575,6 @@ tracemalloc_realloc(int need_gil, void *ctx, void *ptr, size_t new_size)
582575
goto done;
583576
}
584577

585-
PyGILState_STATE gil_state;
586-
if (need_gil) {
587-
gil_state = PyGILState_Ensure();
588-
}
589578
TABLES_LOCK();
590579

591580
if (!tracemalloc_config.tracing) {
@@ -610,8 +599,8 @@ tracemalloc_realloc(int need_gil, void *ctx, void *ptr, size_t new_size)
610599
// This case is very unlikely: a hash entry has just been released,
611600
// so the hash table should have at least one free entry.
612601
//
613-
// The GIL and the table lock ensures that only one thread is
614-
// allocating memory.
602+
// The table lock ensures that no other thread touched the trace
603+
// tables in the meantime.
615604
Py_FatalError("tracemalloc_realloc() failed to allocate a trace");
616605
}
617606
}
@@ -627,9 +616,6 @@ tracemalloc_realloc(int need_gil, void *ctx, void *ptr, size_t new_size)
627616

628617
unlock:
629618
TABLES_UNLOCK();
630-
if (need_gil) {
631-
PyGILState_Release(gil_state);
632-
}
633619

634620
done:
635621
if (!reentrant) {
@@ -665,44 +651,16 @@ tracemalloc_free(void *ctx, void *ptr)
665651

666652

667653
static void*
668-
tracemalloc_malloc_gil(void *ctx, size_t size)
669-
{
670-
return tracemalloc_alloc(0, 0, ctx, 1, size);
671-
}
672-
673-
674-
static void*
675-
tracemalloc_calloc_gil(void *ctx, size_t nelem, size_t elsize)
654+
tracemalloc_malloc(void *ctx, size_t size)
676655
{
677-
return tracemalloc_alloc(0, 1, ctx, nelem, elsize);
656+
return tracemalloc_alloc(0, ctx, 1, size);
678657
}
679658

680659

681660
static void*
682-
tracemalloc_realloc_gil(void *ctx, void *ptr, size_t new_size)
661+
tracemalloc_calloc(void *ctx, size_t nelem, size_t elsize)
683662
{
684-
return tracemalloc_realloc(0, ctx, ptr, new_size);
685-
}
686-
687-
688-
static void*
689-
tracemalloc_raw_malloc(void *ctx, size_t size)
690-
{
691-
return tracemalloc_alloc(1, 0, ctx, 1, size);
692-
}
693-
694-
695-
static void*
696-
tracemalloc_raw_calloc(void *ctx, size_t nelem, size_t elsize)
697-
{
698-
return tracemalloc_alloc(1, 1, ctx, nelem, elsize);
699-
}
700-
701-
702-
static void*
703-
tracemalloc_raw_realloc(void *ctx, void *ptr, size_t new_size)
704-
{
705-
return tracemalloc_realloc(1, ctx, ptr, new_size);
663+
return tracemalloc_alloc(1, ctx, nelem, elsize);
706664
}
707665

708666

@@ -717,7 +675,8 @@ tracemalloc_clear_filename(void *value)
717675
static void
718676
tracemalloc_clear_traces_unlocked(void)
719677
{
720-
// Clearing tracemalloc_filenames requires the GIL to call Py_DECREF()
678+
// Clearing tracemalloc_filenames requires an attached thread state to
679+
// call Py_DECREF()
721680
_Py_AssertHoldsTstate();
722681

723682
set_reentrant(1);
@@ -829,20 +788,15 @@ _PyTraceMalloc_Start(int max_nframe)
829788
}
830789

831790
PyMemAllocatorEx alloc;
832-
alloc.malloc = tracemalloc_raw_malloc;
833-
alloc.calloc = tracemalloc_raw_calloc;
834-
alloc.realloc = tracemalloc_raw_realloc;
791+
alloc.malloc = tracemalloc_malloc;
792+
alloc.calloc = tracemalloc_calloc;
793+
alloc.realloc = tracemalloc_realloc;
835794
alloc.free = tracemalloc_free;
836795

837796
alloc.ctx = &allocators.raw;
838797
PyMem_GetAllocator(PYMEM_DOMAIN_RAW, &allocators.raw);
839798
PyMem_SetAllocator(PYMEM_DOMAIN_RAW, &alloc);
840799

841-
alloc.malloc = tracemalloc_malloc_gil;
842-
alloc.calloc = tracemalloc_calloc_gil;
843-
alloc.realloc = tracemalloc_realloc_gil;
844-
alloc.free = tracemalloc_free;
845-
846800
alloc.ctx = &allocators.mem;
847801
PyMem_GetAllocator(PYMEM_DOMAIN_MEM, &allocators.mem);
848802
PyMem_SetAllocator(PYMEM_DOMAIN_MEM, &alloc);
@@ -1221,7 +1175,6 @@ PyTraceMalloc_Track(unsigned int domain, uintptr_t ptr,
12211175
/* tracemalloc is not tracing: do nothing */
12221176
return -2;
12231177
}
1224-
PyGILState_STATE gil_state = PyGILState_Ensure();
12251178
TABLES_LOCK();
12261179

12271180
int result;
@@ -1234,7 +1187,6 @@ PyTraceMalloc_Track(unsigned int domain, uintptr_t ptr,
12341187
}
12351188

12361189
TABLES_UNLOCK();
1237-
PyGILState_Release(gil_state);
12381190
return result;
12391191
}
12401192

0 commit comments

Comments
 (0)