gh-109649: Add os.process_cpu_count() function (#109907)

* Refactor os_sched_getaffinity_impl(): move variable definitions to
  their first assignment.
* Fix test_posix.test_sched_getaffinity(): restore the old CPU mask
  when the test completes!
* Doc: Specify that os.cpu_count() counts *logicial* CPUs.
* Doc: Specify that os.sched_getaffinity(0) is related to the calling
  thread.
This commit is contained in:
Victor Stinner 2023-10-01 00:12:51 +02:00 committed by GitHub
parent 2c234196ea
commit c81521020d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 125 additions and 47 deletions

View File

@ -5141,8 +5141,12 @@ operating system.
.. function:: sched_getaffinity(pid, /)
Return the set of CPUs the process with PID *pid* (or the current process
if zero) is restricted to.
Return the set of CPUs the process with PID *pid* is restricted to.
If *pid* is zero, return the set of CPUs the calling thread of the current
process is restricted to.
See also the :func:`process_cpu_count` function.
.. _os-path:
@ -5183,12 +5187,11 @@ Miscellaneous System Information
.. function:: cpu_count()
Return the number of CPUs in the system. Returns ``None`` if undetermined.
This number is not equivalent to the number of CPUs the current process can
use. The number of usable CPUs can be obtained with
``len(os.sched_getaffinity(0))``
Return the number of logical CPUs in the **system**. Returns ``None`` if
undetermined.
The :func:`process_cpu_count` function can be used to get the number of
logical CPUs usable by the calling thread of the **current process**.
.. versionadded:: 3.4
@ -5202,6 +5205,20 @@ Miscellaneous System Information
.. availability:: Unix.
.. function:: process_cpu_count()
Get the number of logical CPUs usable by the calling thread of the **current
process**. Returns ``None`` if undetermined. It can be less than
:func:`cpu_count` depending on the CPU affinity.
The :func:`cpu_count` function can be used to get the number of logical CPUs
in the **system**.
See also the :func:`sched_getaffinity` functions.
.. versionadded:: 3.13
.. function:: sysconf(name, /)
Return integer-valued system configuration values. If the configuration value

View File

@ -163,6 +163,13 @@ opcode
documented or exposed through ``dis``, and were not intended to be
used externally.
os
--
* Add :func:`os.process_cpu_count` function to get the number of logical CPUs
usable by the calling thread of the current process.
(Contributed by Victor Stinner in :gh:`109649`.)
pathlib
-------

View File

@ -1136,3 +1136,17 @@ if name == 'nt':
cookie,
nt._remove_dll_directory
)
if _exists('sched_getaffinity'):
def process_cpu_count():
"""
Get the number of CPUs of the current process.
Return the number of logical CPUs usable by the calling thread of the
current process. Return None if indeterminable.
"""
return len(sched_getaffinity(0))
else:
# Just an alias to cpu_count() (same docstring)
process_cpu_count = cpu_count

View File

@ -3996,14 +3996,42 @@ class OSErrorTests(unittest.TestCase):
self.fail(f"No exception thrown by {func}")
class CPUCountTests(unittest.TestCase):
def check_cpu_count(self, cpus):
if cpus is None:
self.skipTest("Could not determine the number of CPUs")
self.assertIsInstance(cpus, int)
self.assertGreater(cpus, 0)
def test_cpu_count(self):
cpus = os.cpu_count()
if cpus is not None:
self.assertIsInstance(cpus, int)
self.assertGreater(cpus, 0)
else:
self.check_cpu_count(cpus)
def test_process_cpu_count(self):
cpus = os.process_cpu_count()
self.assertLessEqual(cpus, os.cpu_count())
self.check_cpu_count(cpus)
@unittest.skipUnless(hasattr(os, 'sched_setaffinity'),
"don't have sched affinity support")
def test_process_cpu_count_affinity(self):
ncpu = os.cpu_count()
if ncpu is None:
self.skipTest("Could not determine the number of CPUs")
# Disable one CPU
mask = os.sched_getaffinity(0)
if len(mask) <= 1:
self.skipTest(f"sched_getaffinity() returns less than "
f"2 CPUs: {sorted(mask)}")
self.addCleanup(os.sched_setaffinity, 0, list(mask))
mask.pop()
os.sched_setaffinity(0, mask)
# test process_cpu_count()
affinity = os.process_cpu_count()
self.assertEqual(affinity, ncpu - 1)
# FD inheritance check is only useful for systems with process support.
@support.requires_subprocess()

View File

@ -1205,6 +1205,7 @@ class PosixTester(unittest.TestCase):
@requires_sched_affinity
def test_sched_setaffinity(self):
mask = posix.sched_getaffinity(0)
self.addCleanup(posix.sched_setaffinity, 0, list(mask))
if len(mask) > 1:
# Empty masks are forbidden
mask.pop()

View File

@ -0,0 +1,2 @@
Add :func:`os.process_cpu_count` function to get the number of logical CPUs
usable by the calling thread of the current process. Patch by Victor Stinner.

View File

@ -10425,11 +10425,9 @@ PyDoc_STRVAR(os_cpu_count__doc__,
"cpu_count($module, /)\n"
"--\n"
"\n"
"Return the number of CPUs in the system; return None if indeterminable.\n"
"Return the number of logical CPUs in the system.\n"
"\n"
"This number is not equivalent to the number of CPUs the current process can\n"
"use. The number of usable CPUs can be obtained with\n"
"``len(os.sched_getaffinity(0))``");
"Return None if indeterminable.");
#define OS_CPU_COUNT_METHODDEF \
{"cpu_count", (PyCFunction)os_cpu_count, METH_NOARGS, os_cpu_count__doc__},
@ -11988,4 +11986,4 @@ exit:
#ifndef OS_WAITSTATUS_TO_EXITCODE_METHODDEF
#define OS_WAITSTATUS_TO_EXITCODE_METHODDEF
#endif /* !defined(OS_WAITSTATUS_TO_EXITCODE_METHODDEF) */
/*[clinic end generated code: output=51aa26bc6a41e1da input=a9049054013a1b77]*/
/*[clinic end generated code: output=8b60de6ddb925bc3 input=a9049054013a1b77]*/

View File

@ -8133,39 +8133,45 @@ static PyObject *
os_sched_getaffinity_impl(PyObject *module, pid_t pid)
/*[clinic end generated code: output=f726f2c193c17a4f input=983ce7cb4a565980]*/
{
int cpu, ncpus, count;
int ncpus = NCPUS_START;
size_t setsize;
cpu_set_t *mask = NULL;
PyObject *res = NULL;
cpu_set_t *mask;
ncpus = NCPUS_START;
while (1) {
setsize = CPU_ALLOC_SIZE(ncpus);
mask = CPU_ALLOC(ncpus);
if (mask == NULL)
if (mask == NULL) {
return PyErr_NoMemory();
if (sched_getaffinity(pid, setsize, mask) == 0)
}
if (sched_getaffinity(pid, setsize, mask) == 0) {
break;
}
CPU_FREE(mask);
if (errno != EINVAL)
if (errno != EINVAL) {
return posix_error();
}
if (ncpus > INT_MAX / 2) {
PyErr_SetString(PyExc_OverflowError, "could not allocate "
"a large enough CPU set");
PyErr_SetString(PyExc_OverflowError,
"could not allocate a large enough CPU set");
return NULL;
}
ncpus = ncpus * 2;
ncpus *= 2;
}
res = PySet_New(NULL);
if (res == NULL)
PyObject *res = PySet_New(NULL);
if (res == NULL) {
goto error;
for (cpu = 0, count = CPU_COUNT_S(setsize, mask); count; cpu++) {
}
int cpu = 0;
int count = CPU_COUNT_S(setsize, mask);
for (; count; cpu++) {
if (CPU_ISSET_S(cpu, setsize, mask)) {
PyObject *cpu_num = PyLong_FromLong(cpu);
--count;
if (cpu_num == NULL)
if (cpu_num == NULL) {
goto error;
}
if (PySet_Add(res, cpu_num)) {
Py_DECREF(cpu_num);
goto error;
@ -8177,12 +8183,12 @@ os_sched_getaffinity_impl(PyObject *module, pid_t pid)
return res;
error:
if (mask)
if (mask) {
CPU_FREE(mask);
}
Py_XDECREF(res);
return NULL;
}
#endif /* HAVE_SCHED_SETAFFINITY */
#endif /* HAVE_SCHED_H */
@ -14333,44 +14339,49 @@ os_get_terminal_size_impl(PyObject *module, int fd)
/*[clinic input]
os.cpu_count
Return the number of CPUs in the system; return None if indeterminable.
Return the number of logical CPUs in the system.
This number is not equivalent to the number of CPUs the current process can
use. The number of usable CPUs can be obtained with
``len(os.sched_getaffinity(0))``
Return None if indeterminable.
[clinic start generated code]*/
static PyObject *
os_cpu_count_impl(PyObject *module)
/*[clinic end generated code: output=5fc29463c3936a9c input=e7c8f4ba6dbbadd3]*/
/*[clinic end generated code: output=5fc29463c3936a9c input=ba2f6f8980a0e2eb]*/
{
int ncpu = 0;
int ncpu;
#ifdef MS_WINDOWS
#ifdef MS_WINDOWS_DESKTOP
# ifdef MS_WINDOWS_DESKTOP
ncpu = GetActiveProcessorCount(ALL_PROCESSOR_GROUPS);
#endif
# else
ncpu = 0;
# endif
#elif defined(__hpux)
ncpu = mpctl(MPC_GETNUMSPUS, NULL, NULL);
#elif defined(HAVE_SYSCONF) && defined(_SC_NPROCESSORS_ONLN)
ncpu = sysconf(_SC_NPROCESSORS_ONLN);
#elif defined(__VXWORKS__)
ncpu = _Py_popcount32(vxCpuEnabledGet());
#elif defined(__DragonFly__) || \
defined(__OpenBSD__) || \
defined(__FreeBSD__) || \
defined(__NetBSD__) || \
defined(__APPLE__)
int mib[2];
ncpu = 0;
size_t len = sizeof(ncpu);
mib[0] = CTL_HW;
mib[1] = HW_NCPU;
if (sysctl(mib, 2, &ncpu, &len, NULL, 0) != 0)
int mib[2] = {CTL_HW, HW_NCPU};
if (sysctl(mib, 2, &ncpu, &len, NULL, 0) != 0) {
ncpu = 0;
}
#endif
if (ncpu >= 1)
return PyLong_FromLong(ncpu);
else
if (ncpu < 1) {
Py_RETURN_NONE;
}
return PyLong_FromLong(ncpu);
}