Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 38 additions & 9 deletions Doc/c-api/memory.rst
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,10 @@ The following function sets are wrappers to the system allocator. These
functions are thread-safe, the :term:`GIL <global interpreter lock>` does not
need to be held.

The default raw memory block allocator uses the following functions:
:c:func:`malloc`, :c:func:`calloc`, :c:func:`realloc` and :c:func:`free`; call
``malloc(1)`` (or ``calloc(1, 1)``) when requesting zero bytes.
The :ref:`default raw memory allocator <default-memory-allocators>` uses
the following functions: :c:func:`malloc`, :c:func:`calloc`, :c:func:`realloc`
and :c:func:`free`; call ``malloc(1)`` (or ``calloc(1, 1)``) when requesting
zero bytes.

.. versionadded:: 3.4

Expand Down Expand Up @@ -165,7 +166,8 @@ The following function sets, modeled after the ANSI C standard, but specifying
behavior when requesting zero bytes, are available for allocating and releasing
memory from the Python heap.

By default, these functions use :ref:`pymalloc memory allocator <pymalloc>`.
The :ref:`default memory allocator <default-memory-allocators>` uses the
:ref:`pymalloc memory allocator <pymalloc>`.

.. warning::

Expand Down Expand Up @@ -270,7 +272,8 @@ The following function sets, modeled after the ANSI C standard, but specifying
behavior when requesting zero bytes, are available for allocating and releasing
memory from the Python heap.

By default, these functions use :ref:`pymalloc memory allocator <pymalloc>`.
The :ref:`default object allocator <default-memory-allocators>` uses the
:ref:`pymalloc memory allocator <pymalloc>`.

.. warning::

Expand Down Expand Up @@ -326,6 +329,31 @@ By default, these functions use :ref:`pymalloc memory allocator <pymalloc>`.
If *p* is *NULL*, no operation is performed.


.. _default-memory-allocators:

Default Memory Allocators
=========================

Default memory allocators:

=============================== ==================== ================== ===================== ====================
Configuration Name PyMem_RawMalloc PyMem_Malloc PyObject_Malloc
=============================== ==================== ================== ===================== ====================
Release build ``"pymalloc"`` ``malloc`` ``pymalloc`` ``pymalloc``
Debug build ``"pymalloc_debug"`` ``malloc`` + debug ``pymalloc`` + debug ``pymalloc`` + debug
Release build, without pymalloc ``"malloc"`` ``malloc`` ``malloc`` ``malloc``
Release build, without pymalloc ``"malloc_debug"`` ``malloc`` + debug ``malloc`` + debug ``malloc`` + debug
=============================== ==================== ================== ===================== ====================

Legend:

* Name: value for :envvar:`PYTHONMALLOC` environment variable
* ``malloc``: system allocators from the standard C library, C functions:
:c:func:`malloc`, :c:func:`calloc`, :c:func:`realloc` and :c:func:`free`
* ``pymalloc``: :ref:`pymalloc memory allocator <pymalloc>`
* "+ debug": with debug hooks installed by :c:func:`PyMem_SetupDebugHooks`


Customize Memory Allocators
===========================

Expand Down Expand Up @@ -431,7 +459,8 @@ Customize Memory Allocators
displayed if :mod:`tracemalloc` is tracing Python memory allocations and the
memory block was traced.

These hooks are installed by default if Python is compiled in debug
These hooks are :ref:`installed by default <default-memory-allocators>` if
Python is compiled in debug
mode. The :envvar:`PYTHONMALLOC` environment variable can be used to install
debug hooks on a Python compiled in release mode.

Expand All @@ -453,9 +482,9 @@ to 512 bytes) with a short lifetime. It uses memory mappings called "arenas"
with a fixed size of 256 KiB. It falls back to :c:func:`PyMem_RawMalloc` and
:c:func:`PyMem_RawRealloc` for allocations larger than 512 bytes.

*pymalloc* is the default allocator of the :c:data:`PYMEM_DOMAIN_MEM` (ex:
:c:func:`PyMem_Malloc`) and :c:data:`PYMEM_DOMAIN_OBJ` (ex:
:c:func:`PyObject_Malloc`) domains.
*pymalloc* is the :ref:`default allocator <default-memory-allocators>` of the
:c:data:`PYMEM_DOMAIN_MEM` (ex: :c:func:`PyMem_Malloc`) and
:c:data:`PYMEM_DOMAIN_OBJ` (ex: :c:func:`PyObject_Malloc`) domains.

The arena allocator uses the following functions:

Expand Down
19 changes: 9 additions & 10 deletions Doc/using/cmdline.rst
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,8 @@ conflict.

Set the family of memory allocators used by Python:

* ``default``: use the :ref:`default memory allocators
<default-memory-allocators>`.
* ``malloc``: use the :c:func:`malloc` function of the C library
for all domains (:c:data:`PYMEM_DOMAIN_RAW`, :c:data:`PYMEM_DOMAIN_MEM`,
:c:data:`PYMEM_DOMAIN_OBJ`).
Expand All @@ -696,20 +698,17 @@ conflict.

Install debug hooks:

* ``debug``: install debug hooks on top of the default memory allocator
* ``debug``: install debug hooks on top of the :ref:`default memory
allocators <default-memory-allocators>`.
* ``malloc_debug``: same as ``malloc`` but also install debug hooks
* ``pymalloc_debug``: same as ``pymalloc`` but also install debug hooks

When Python is compiled in release mode, the default is ``pymalloc``. When
compiled in debug mode, the default is ``pymalloc_debug`` and the debug hooks
are used automatically.
See the :ref:`default memory allocators <default-memory-allocators>` and the
:c:func:`PyMem_SetupDebugHooks` function (install debug hooks on Python
memory allocators).

If Python is configured without ``pymalloc`` support, ``pymalloc`` and
``pymalloc_debug`` are not available, the default is ``malloc`` in release
mode and ``malloc_debug`` in debug mode.

See the :c:func:`PyMem_SetupDebugHooks` function for debug hooks on Python
memory allocators.
.. versionchanged:: 3.7
Added the ``"default"`` allocator.

.. versionadded:: 3.6

Expand Down
10 changes: 9 additions & 1 deletion Include/pymem.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ PyAPI_FUNC(void) PyMem_RawFree(void *ptr);
allocators. */
PyAPI_FUNC(int) _PyMem_SetupAllocators(const char *opt);

/* Try to get the allocators name set by _PyMem_SetupAllocators(). */
PyAPI_FUNC(const char*) _PyMem_GetAllocatorsName(void);

#ifdef WITH_PYMALLOC
PyAPI_FUNC(int) _PyMem_PymallocEnabled(void);
#endif
Expand Down Expand Up @@ -230,7 +233,12 @@ PyAPI_FUNC(void) PyMem_SetupDebugHooks(void);
#endif

#ifdef Py_BUILD_CORE
PyAPI_FUNC(void) _PyMem_GetDefaultRawAllocator(PyMemAllocatorEx *alloc);
/* Set the memory allocator of the specified domain to the default.
Save the old allocator into *old_alloc if it's non-NULL.
Return on success, or return -1 if the domain is unknown. */
PyAPI_FUNC(int) _PyMem_SetDefaultAllocator(
PyMemAllocatorDomain domain,
PyMemAllocatorEx *old_alloc);
#endif

#ifdef __cplusplus
Expand Down
50 changes: 32 additions & 18 deletions Lib/test/pythoninfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ def copy_attributes(info_add, obj, name_fmt, attributes, *, formatter=None):
info_add(name, value)


def copy_attr(info_add, name, mod, attr_name):
try:
value = getattr(mod, attr_name)
except AttributeError:
return
info_add(name, value)


def call_func(info_add, name, mod, func_name, *, formatter=None):
try:
func = getattr(mod, func_name)
Expand Down Expand Up @@ -168,11 +176,10 @@ def format_attr(attr, value):
call_func(info_add, 'os.gid', os, 'getgid')
call_func(info_add, 'os.uname', os, 'uname')

if hasattr(os, 'getgroups'):
groups = os.getgroups()
groups = map(str, groups)
groups = ', '.join(groups)
info_add("os.groups", groups)
def format_groups(groups):
return ', '.join(map(str, groups))

call_func(info_add, 'os.groups', os, 'getgroups', formatter=format_groups)

if hasattr(os, 'getlogin'):
try:
Expand All @@ -184,11 +191,7 @@ def format_attr(attr, value):
else:
info_add("os.login", login)

if hasattr(os, 'cpu_count'):
cpu_count = os.cpu_count()
if cpu_count:
info_add('os.cpu_count', cpu_count)

call_func(info_add, 'os.cpu_count', os, 'cpu_count')
call_func(info_add, 'os.loadavg', os, 'getloadavg')

# Get environment variables: filter to list
Expand Down Expand Up @@ -219,7 +222,9 @@ def format_attr(attr, value):
)
for name, value in os.environ.items():
uname = name.upper()
if (uname in ENV_VARS or uname.startswith(("PYTHON", "LC_"))
if (uname in ENV_VARS
# Copy PYTHON* and LC_* variables
or uname.startswith(("PYTHON", "LC_"))
# Visual Studio: VS140COMNTOOLS
or (uname.startswith("VS") and uname.endswith("COMNTOOLS"))):
info_add('os.environ[%s]' % name, value)
Expand Down Expand Up @@ -313,12 +318,10 @@ def collect_time(info_add):
)
copy_attributes(info_add, time, 'time.%s', attributes)

if not hasattr(time, 'get_clock_info'):
return

for clock in ('time', 'perf_counter'):
tinfo = time.get_clock_info(clock)
info_add('time.%s' % clock, tinfo)
if hasattr(time, 'get_clock_info'):
for clock in ('time', 'perf_counter'):
tinfo = time.get_clock_info(clock)
info_add('time.%s' % clock, tinfo)


def collect_sysconfig(info_add):
Expand All @@ -331,14 +334,14 @@ def collect_sysconfig(info_add):
'CCSHARED',
'CFLAGS',
'CFLAGSFORSHARED',
'PY_LDFLAGS',
'CONFIG_ARGS',
'HOST_GNU_TYPE',
'MACHDEP',
'MULTIARCH',
'OPT',
'PY_CFLAGS',
'PY_CFLAGS_NODIST',
'PY_LDFLAGS',
'Py_DEBUG',
'Py_ENABLE_SHARED',
'SHELL',
Expand Down Expand Up @@ -422,6 +425,16 @@ def collect_decimal(info_add):
copy_attributes(info_add, _decimal, '_decimal.%s', attributes)


def collect_testcapi(info_add):
try:
import _testcapi
except ImportError:
return

call_func(info_add, 'pymem.allocator', _testcapi, 'pymem_getallocatorsname')
copy_attr(info_add, 'pymem.with_pymalloc', _testcapi, 'WITH_PYMALLOC')


def collect_info(info):
error = False
info_add = info.add
Expand All @@ -444,6 +457,7 @@ def collect_info(info):
collect_zlib,
collect_expat,
collect_decimal,
collect_testcapi,
):
try:
collect_func(info_add)
Expand Down
5 changes: 5 additions & 0 deletions Lib/test/support/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2861,3 +2861,8 @@ def save(self):
def restore(self):
for signum, handler in self.handlers.items():
self.signal.signal(signum, handler)


def with_pymalloc():
import _testcapi
return _testcapi.WITH_PYMALLOC
3 changes: 1 addition & 2 deletions Lib/test/test_capi.py
Original file line number Diff line number Diff line change
Expand Up @@ -654,8 +654,7 @@ class PyMemMallocDebugTests(PyMemDebugTests):
PYTHONMALLOC = 'malloc_debug'


@unittest.skipUnless(sysconfig.get_config_var('WITH_PYMALLOC') == 1,
'need pymalloc')
@unittest.skipUnless(support.with_pymalloc(), 'need pymalloc')
class PyMemPymallocDebugTests(PyMemDebugTests):
PYTHONMALLOC = 'pymalloc_debug'

Expand Down
52 changes: 50 additions & 2 deletions Lib/test/test_cmd_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import os
import subprocess
import sys
import sysconfig
import tempfile
import unittest
from test import support
Expand Down Expand Up @@ -559,10 +560,14 @@ def test_xdev(self):
except ImportError:
pass
else:
code = "import _testcapi; _testcapi.pymem_api_misuse()"
code = "import _testcapi; print(_testcapi.pymem_getallocatorsname())"
with support.SuppressCrashReport():
out = self.run_xdev("-c", code, check_exitcode=False)
self.assertIn("Debug memory block at address p=", out)
if support.with_pymalloc():
alloc_name = "pymalloc_debug"
else:
alloc_name = "malloc_debug"
self.assertEqual(out, alloc_name)

try:
import faulthandler
Expand All @@ -573,6 +578,49 @@ def test_xdev(self):
out = self.run_xdev("-c", code)
self.assertEqual(out, "True")

def check_pythonmalloc(self, env_var, name):
code = 'import _testcapi; print(_testcapi.pymem_getallocatorsname())'
env = dict(os.environ)
if env_var is not None:
env['PYTHONMALLOC'] = env_var
else:
env.pop('PYTHONMALLOC', None)
args = (sys.executable, '-c', code)
proc = subprocess.run(args,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True,
env=env)
self.assertEqual(proc.stdout.rstrip(), name)
self.assertEqual(proc.returncode, 0)

def test_pythonmalloc(self):
# Test the PYTHONMALLOC environment variable
pydebug = hasattr(sys, "gettotalrefcount")
pymalloc = support.with_pymalloc()
if pymalloc:
default_name = 'pymalloc_debug' if pydebug else 'pymalloc'
default_name_debug = 'pymalloc_debug'
else:
default_name = 'malloc_debug' if pydebug else 'malloc'
default_name_debug = 'malloc_debug'

tests = [
(None, default_name),
('debug', default_name_debug),
('malloc', 'malloc'),
('malloc_debug', 'malloc_debug'),
]
if pymalloc:
tests.extend((
('pymalloc', 'pymalloc'),
('pymalloc_debug', 'pymalloc_debug'),
))

for env_var, name in tests:
with self.subTest(env_var=env_var, name=name):
self.check_pythonmalloc(env_var, name)


class IgnoreEnvironmentTest(unittest.TestCase):

Expand Down
9 changes: 8 additions & 1 deletion Lib/test/test_sys.py
Original file line number Diff line number Diff line change
Expand Up @@ -753,8 +753,15 @@ def test_debugmallocstats(self):
@unittest.skipUnless(hasattr(sys, "getallocatedblocks"),
"sys.getallocatedblocks unavailable on this build")
def test_getallocatedblocks(self):
try:
import _testcapi
except ImportError:
with_pymalloc = support.with_pymalloc()
else:
alloc_name = _testcapi.pymem_getallocatorsname()
with_pymalloc = (alloc_name in ('pymalloc', 'pymalloc_debug'))

# Some sanity checks
with_pymalloc = sysconfig.get_config_var('WITH_PYMALLOC')
a = sys.getallocatedblocks()
self.assertIs(type(a), int)
if with_pymalloc:
Expand Down
Loading