Skip to content

Commit 6b2b68a

Browse files
Subbarao Garlapatimeta-codesync[bot]
authored andcommitted
Add sys.getallocatedbytes
Summary: Port `sys.getallocatedbytes()` from 3.12 (added to 3.12 in D60045182), with a few changes - `mimalloc` is new in 3.14 so check the allocations from there - General FT support (in `mimalloc` as well as stop the world in `get_global_allocated_bytes()`) - Instead of adding a relaxed add to pyatomic, using the helper function `raw_atomic_add_relaxed()` which calls `__ATOMIC_RELAXED` directly. This keeps the patch more maintainable. Note that for MSVC this is a sequential consistent atomic add (which is the same as a relaxed atomic add on x86, but not on ARM - the MSVC/ARM64 build isn't supported by CPython so this should be fine). **How the method works:** Sum up the three places of memory allocation across all interpreter states by: - Adding the `raw_allocated_bytes` value - direct memory allocations using `_Py_Raw*` methods are tracked here - Add the sum of the sizes of all the blocks used by `mimalloc` as well as its abandoned pool - Add the sum of the sizes of all pools in all active arenas (`pymalloc`'s allocations) **Justification for adding the method to 3.14:** `sys.getallocatedbytes()` is used in Imports Monitor for memory measurement Reviewed By: kddnewton Differential Revision: D112405580 fbshipit-source-id: e284ade7440de2a22d389ecdd88813dc9f830b52
1 parent ee58218 commit 6b2b68a

5 files changed

Lines changed: 274 additions & 3 deletions

File tree

Include/internal/pycore_obmalloc.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -682,9 +682,11 @@ void _PyObject_VirtualFree(void *, size_t size);
682682

683683
/* This function returns the number of allocated memory blocks, regardless of size */
684684
extern Py_ssize_t _Py_GetGlobalAllocatedBlocks(void);
685+
extern Py_ssize_t _Py_GetGlobalAllocatedBytes(void);
685686
#define _Py_GetAllocatedBlocks() \
686687
_Py_GetGlobalAllocatedBlocks()
687688
extern Py_ssize_t _PyInterpreterState_GetAllocatedBlocks(PyInterpreterState *);
689+
extern Py_ssize_t _PyInterpreterState_GetAllocatedBytes(PyInterpreterState *);
688690
extern void _PyInterpreterState_FinalizeAllocatedBlocks(PyInterpreterState *);
689691
extern int _PyMem_init_obmalloc(PyInterpreterState *interp);
690692
extern bool _PyMem_obmalloc_state_on_heap(PyInterpreterState *interp);

Lib/test/test_sys.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1154,6 +1154,36 @@ def test_getallocatedblocks(self):
11541154
c = sys.getallocatedblocks()
11551155
self.assertIn(c, range(b - 50, b + 50))
11561156

1157+
@unittest.skipUnless(hasattr(sys, "getallocatedbytes"),
1158+
"sys.getallocatedbytes unavailable on this build")
1159+
def test_getallocatedbytes(self):
1160+
a = sys.getallocatedbytes()
1161+
self.assertIs(type(a), int)
1162+
self.assertGreater(a, 0)
1163+
gc.collect()
1164+
b = sys.getallocatedbytes()
1165+
self.assertLessEqual(b, a)
1166+
# raw memory
1167+
o = "." * 1000000
1168+
z = sys.getsizeof(o)
1169+
gc.collect()
1170+
c = sys.getallocatedbytes()
1171+
self.assertGreater(c - b + 100, z - 100)
1172+
del o
1173+
gc.collect()
1174+
d = sys.getallocatedbytes()
1175+
self.assertGreater(c - d + 100, z - 100)
1176+
# small objects memory
1177+
o = [f"{i:^4}" for i in range(1000)]
1178+
z = sys.getsizeof(o) + sys.getsizeof(o[0]) * len(o)
1179+
gc.collect()
1180+
e = sys.getallocatedbytes()
1181+
self.assertGreater(e - d + 100, z - 100)
1182+
del o
1183+
gc.collect()
1184+
f = sys.getallocatedbytes()
1185+
self.assertGreater(e - f + 100, z - 100)
1186+
11571187
def test_is_gil_enabled(self):
11581188
if support.Py_GIL_DISABLED:
11591189
self.assertIs(type(sys._is_gil_enabled()), bool)

Objects/obmalloc.c

Lines changed: 198 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212

1313
#include <stdlib.h> // malloc()
1414
#include <stdbool.h>
15+
#ifdef __linux__
16+
# include <malloc.h> // malloc_usable_size()
17+
#elif defined(__APPLE__)
18+
# include <malloc/malloc.h> // malloc_size()
19+
#endif
1520
#ifdef WITH_MIMALLOC
1621
// Forward declarations of functions used in our mimalloc modifications
1722
static void _PyMem_mi_page_clear_qsbr(mi_page_t *page);
@@ -38,6 +43,36 @@ extern void _PyMem_DumpTraceback(int fd, const void *ptr);
3843
static void _PyObject_DebugDumpAddress(const void *p);
3944
static void _PyMem_DebugCheckAddress(const char *func, char api_id, const void *p);
4045

46+
static inline size_t
47+
raw_malloc_size(void *p)
48+
{
49+
if (p != NULL) {
50+
#ifdef MS_WINDOWS
51+
return _msize(p);
52+
#elif defined(__linux__)
53+
return malloc_usable_size(p);
54+
#elif defined(__APPLE__)
55+
return malloc_size(p);
56+
#endif
57+
}
58+
return 0;
59+
}
60+
61+
static uint64_t raw_allocated_bytes;
62+
63+
/* Relaxed atomic add for raw_allocated_bytes: pyatomic has no relaxed add,
64+
so avoid a barrier on this allocation hot path by using the compiler
65+
builtin directly. MSVC falls back to seq_cst, identical to relaxed on x86. */
66+
static inline void
67+
raw_atomic_add_relaxed(uint64_t *obj, uint64_t value)
68+
{
69+
#if defined(__GNUC__) || defined(__clang__)
70+
(void)__atomic_fetch_add(obj, value, __ATOMIC_RELAXED);
71+
#else
72+
(void)_Py_atomic_add_uint64(obj, value); // seq_cst; same as relaxed on x86
73+
#endif
74+
}
75+
4176

4277
static void set_up_debug_hooks_domain_unlocked(PyMemAllocatorDomain domain);
4378
static void set_up_debug_hooks_unlocked(void);
@@ -60,7 +95,11 @@ _PyMem_RawMalloc(void *Py_UNUSED(ctx), size_t size)
6095
To solve these problems, allocate an extra byte. */
6196
if (size == 0)
6297
size = 1;
63-
return malloc(size);
98+
void *ptr = malloc(size);
99+
if (ptr != NULL) {
100+
raw_atomic_add_relaxed(&raw_allocated_bytes, raw_malloc_size(ptr));
101+
}
102+
return ptr;
64103
}
65104

66105
void *
@@ -74,21 +113,33 @@ _PyMem_RawCalloc(void *Py_UNUSED(ctx), size_t nelem, size_t elsize)
74113
nelem = 1;
75114
elsize = 1;
76115
}
77-
return calloc(nelem, elsize);
116+
void *ptr = calloc(nelem, elsize);
117+
if (ptr != NULL) {
118+
raw_atomic_add_relaxed(&raw_allocated_bytes, raw_malloc_size(ptr));
119+
}
120+
return ptr;
78121
}
79122

80123
void *
81124
_PyMem_RawRealloc(void *Py_UNUSED(ctx), void *ptr, size_t size)
82125
{
83126
if (size == 0)
84127
size = 1;
85-
return realloc(ptr, size);
128+
size_t oldsize = raw_malloc_size(ptr);
129+
ptr = realloc(ptr, size);
130+
if (ptr != NULL) {
131+
raw_atomic_add_relaxed(&raw_allocated_bytes, raw_malloc_size(ptr));
132+
raw_atomic_add_relaxed(&raw_allocated_bytes, 0 - (uint64_t)oldsize);
133+
}
134+
return ptr;
86135
}
87136

88137
void
89138
_PyMem_RawFree(void *Py_UNUSED(ctx), void *ptr)
90139
{
140+
size_t size = raw_malloc_size(ptr);
91141
free(ptr);
142+
raw_atomic_add_relaxed(&raw_allocated_bytes, 0 - (uint64_t)size);
92143
}
93144

94145
#ifdef WITH_MIMALLOC
@@ -1592,6 +1643,14 @@ static bool count_blocks(
15921643
return 1;
15931644
}
15941645

1646+
static bool count_bytes(
1647+
const mi_heap_t* heap, const mi_heap_area_t* area,
1648+
void* block, size_t block_size, void* allocated_bytes)
1649+
{
1650+
*(size_t *)allocated_bytes += area->used * block_size;
1651+
return 1;
1652+
}
1653+
15951654
static Py_ssize_t
15961655
get_mimalloc_allocated_blocks(PyInterpreterState *interp)
15971656
{
@@ -1617,6 +1676,32 @@ get_mimalloc_allocated_blocks(PyInterpreterState *interp)
16171676
#endif
16181677
return allocated_blocks;
16191678
}
1679+
1680+
static Py_ssize_t
1681+
get_mimalloc_allocated_bytes(PyInterpreterState *interp)
1682+
{
1683+
size_t allocated_bytes = 0;
1684+
#ifdef Py_GIL_DISABLED
1685+
_Py_FOR_EACH_TSTATE_UNLOCKED(interp, t) {
1686+
_PyThreadStateImpl *tstate = (_PyThreadStateImpl *)t;
1687+
for (int i = 0; i < _Py_MIMALLOC_HEAP_COUNT; i++) {
1688+
mi_heap_t *heap = &tstate->mimalloc.heaps[i];
1689+
mi_heap_visit_blocks(heap, false, &count_bytes, &allocated_bytes);
1690+
}
1691+
}
1692+
1693+
mi_abandoned_pool_t *pool = &interp->mimalloc.abandoned_pool;
1694+
for (uint8_t tag = 0; tag < _Py_MIMALLOC_HEAP_COUNT; tag++) {
1695+
_mi_abandoned_pool_visit_blocks(pool, tag, false, &count_bytes,
1696+
&allocated_bytes);
1697+
}
1698+
#else
1699+
// Same limitation as get_mimalloc_allocated_blocks: only counts the current thread's bytes.
1700+
mi_heap_t *heap = mi_heap_get_default();
1701+
mi_heap_visit_blocks(heap, false, &count_bytes, &allocated_bytes);
1702+
#endif
1703+
return allocated_bytes;
1704+
}
16201705
#endif
16211706

16221707
Py_ssize_t
@@ -1662,6 +1747,49 @@ _PyInterpreterState_GetAllocatedBlocks(PyInterpreterState *interp)
16621747
return n;
16631748
}
16641749

1750+
Py_ssize_t
1751+
_PyInterpreterState_GetAllocatedBytes(PyInterpreterState *interp)
1752+
{
1753+
#ifdef WITH_MIMALLOC
1754+
if (_PyMem_MimallocEnabled()) {
1755+
return get_mimalloc_allocated_bytes(interp);
1756+
}
1757+
#endif
1758+
1759+
#ifdef Py_DEBUG
1760+
assert(has_own_state(interp));
1761+
#else
1762+
if (!has_own_state(interp)) {
1763+
_Py_FatalErrorFunc(__func__,
1764+
"the interpreter doesn't have its own allocator");
1765+
}
1766+
#endif
1767+
OMState *state = interp->obmalloc;
1768+
1769+
if (state == NULL) {
1770+
return 0;
1771+
}
1772+
1773+
Py_ssize_t n = 0;
1774+
/* add up allocated bytes for used pools */
1775+
for (uint i = 0; i < maxarenas; ++i) {
1776+
/* Skip arenas which are not allocated. */
1777+
if (allarenas[i].address == 0) {
1778+
continue;
1779+
}
1780+
1781+
uintptr_t base = (uintptr_t)_Py_ALIGN_UP(allarenas[i].address, POOL_SIZE);
1782+
1783+
/* visit every pool in the arena */
1784+
assert(base <= (uintptr_t) allarenas[i].pool_address);
1785+
for (; base < (uintptr_t) allarenas[i].pool_address; base += POOL_SIZE) {
1786+
poolp p = (poolp)base;
1787+
n += (p->ref.count * INDEX2SIZE(p->szidx));
1788+
}
1789+
}
1790+
return n;
1791+
}
1792+
16651793
static void free_obmalloc_arenas(PyInterpreterState *interp);
16661794

16671795
void
@@ -1763,6 +1891,61 @@ _Py_GetGlobalAllocatedBlocks(void)
17631891
return get_num_global_allocated_blocks(&_PyRuntime);
17641892
}
17651893

1894+
static Py_ssize_t
1895+
get_global_allocated_bytes(_PyRuntimeState *runtime)
1896+
{
1897+
Py_ssize_t total = (Py_ssize_t)_Py_atomic_load_uint64_relaxed(&raw_allocated_bytes);
1898+
if (_PyRuntimeState_GetFinalizing(runtime) != NULL) {
1899+
PyInterpreterState *interp = _PyInterpreterState_Main();
1900+
if (interp == NULL) {
1901+
/* We are at the very end of runtime finalization.
1902+
We can't rely on finalizing->interp since that thread
1903+
state is probably already freed, so we don't worry
1904+
about it. */
1905+
assert(PyInterpreterState_Head() == NULL);
1906+
}
1907+
else {
1908+
assert(interp != NULL);
1909+
/* It is probably the last interpreter but not necessarily. */
1910+
assert(PyInterpreterState_Next(interp) == NULL);
1911+
total += _PyInterpreterState_GetAllocatedBytes(interp);
1912+
}
1913+
}
1914+
else {
1915+
_PyEval_StopTheWorldAll(&_PyRuntime);
1916+
HEAD_LOCK(runtime);
1917+
PyInterpreterState *interp = PyInterpreterState_Head();
1918+
assert(interp != NULL);
1919+
#ifdef Py_DEBUG
1920+
int got_main = 0;
1921+
#endif
1922+
for (; interp != NULL; interp = PyInterpreterState_Next(interp)) {
1923+
#ifdef Py_DEBUG
1924+
if (_Py_IsMainInterpreter(interp)) {
1925+
assert(!got_main);
1926+
got_main = 1;
1927+
assert(has_own_state(interp));
1928+
}
1929+
#endif
1930+
if (has_own_state(interp)) {
1931+
total += _PyInterpreterState_GetAllocatedBytes(interp);
1932+
}
1933+
}
1934+
HEAD_UNLOCK(runtime);
1935+
_PyEval_StartTheWorldAll(&_PyRuntime);
1936+
#ifdef Py_DEBUG
1937+
assert(got_main);
1938+
#endif
1939+
}
1940+
return total;
1941+
}
1942+
1943+
Py_ssize_t
1944+
_Py_GetGlobalAllocatedBytes(void)
1945+
{
1946+
return get_global_allocated_bytes(&_PyRuntime);
1947+
}
1948+
17661949
#if WITH_PYMALLOC_RADIX_TREE
17671950
/*==========================================================================*/
17681951
/* radix tree for tracking arena usage. */
@@ -2730,6 +2913,18 @@ _Py_GetGlobalAllocatedBlocks(void)
27302913
return 0;
27312914
}
27322915

2916+
Py_ssize_t
2917+
_PyInterpreterState_GetAllocatedBytes(PyInterpreterState *Py_UNUSED(interp))
2918+
{
2919+
return 0;
2920+
}
2921+
2922+
Py_ssize_t
2923+
_Py_GetGlobalAllocatedBytes(void)
2924+
{
2925+
return (Py_ssize_t)_Py_atomic_load_uint64_relaxed(&raw_allocated_bytes);
2926+
}
2927+
27332928
void
27342929
_PyInterpreterState_FinalizeAllocatedBlocks(PyInterpreterState *Py_UNUSED(interp))
27352930
{

Python/clinic/sysmodule.c.h

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

Python/sysmodule.c

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2057,6 +2057,21 @@ sys_getallocatedblocks_impl(PyObject *module)
20572057
return _Py_GetGlobalAllocatedBlocks();
20582058
}
20592059

2060+
/*[clinic input]
2061+
sys.getallocatedbytes -> Py_ssize_t
2062+
2063+
Return the number of memory bytes currently allocated.
2064+
[clinic start generated code]*/
2065+
2066+
static Py_ssize_t
2067+
sys_getallocatedbytes_impl(PyObject *module)
2068+
/*[clinic end generated code: output=a340d143783a5d72 input=7c169a544662d3e2]*/
2069+
{
2070+
// It might make sense to return the bytes
2071+
// for just the current interpreter.
2072+
return _Py_GetGlobalAllocatedBytes();
2073+
}
2074+
20602075
/*[clinic input]
20612076
sys.getunicodeinternedsize -> Py_ssize_t
20622077
@@ -2839,6 +2854,7 @@ static PyMethodDef sys_methods[] = {
28392854
SYS_GETDEFAULTENCODING_METHODDEF
28402855
SYS_GETDLOPENFLAGS_METHODDEF
28412856
SYS_GETALLOCATEDBLOCKS_METHODDEF
2857+
SYS_GETALLOCATEDBYTES_METHODDEF
28422858
SYS_GETUNICODEINTERNEDSIZE_METHODDEF
28432859
SYS_GETFILESYSTEMENCODING_METHODDEF
28442860
SYS_GETFILESYSTEMENCODEERRORS_METHODDEF

0 commit comments

Comments
 (0)