Skip to content

Commit

Permalink
pythongh-109649: Add os.process_cpu_count() function
Browse files Browse the repository at this point in the history
* Fix test_posix.test_sched_getaffinity(): restore the old CPU mask
  when the test completes!
* Doc: Specify that os.cpu_count() counts *logicial* CPUs.
  • Loading branch information
vstinner committed Sep 26, 2023
1 parent 19bf398 commit e3554fa
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 45 deletions.
19 changes: 14 additions & 5 deletions Doc/library/os.rst
Expand Up @@ -5183,12 +5183,10 @@ 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.

See also the :func:`process_cpu_count` function.

.. versionadded:: 3.4

Expand All @@ -5202,6 +5200,17 @@ Miscellaneous System Information
.. availability:: Unix.


.. function:: process_cpu_count()

Get the number of logical CPUs usable by the current process. Returns
``None`` if undetermined. It can be less than :func:`cpu_count` depending on
the process affinity.

See also the :func:`cpu_count` function.

.. versionadded:: 3.13


.. function:: sysconf(name, /)

Return integer-valued system configuration values. If the configuration value
Expand Down
7 changes: 7 additions & 0 deletions Doc/whatsnew/3.13.rst
Expand Up @@ -163,6 +163,13 @@ opcode
documented or exposed through ``dis``, and were not intended to be
used externally.

os
--

* Add :func:`process_cpu_count` function to get the number of logical CPUs
usable by the current process.
(Contributed by Victor Stinner in :gh:`109649`.)

pathlib
-------

Expand Down
36 changes: 32 additions & 4 deletions Lib/test/test_os.py
Expand Up @@ -3996,14 +3996,42 @@ def test_oserror_filename(self):
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()
Expand Down
1 change: 1 addition & 0 deletions Lib/test/test_posix.py
Expand Up @@ -1205,6 +1205,7 @@ def test_sched_getaffinity(self):
@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()
Expand Down
@@ -0,0 +1,2 @@
Add :func:`process_cpu_count` function to get the number of logical CPUs usable
by the current process. Patch by Victor Stinner.
28 changes: 23 additions & 5 deletions Modules/clinic/posixmodule.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

121 changes: 90 additions & 31 deletions Modules/posixmodule.c
Expand Up @@ -8138,39 +8138,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;
Expand All @@ -8182,12 +8188,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 */
Expand Down Expand Up @@ -14338,44 +14344,96 @@ 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);
}


/*[clinic input]
os.process_cpu_count
Get the number of logical CPUs usable by the current process.
Return None if indeterminable.
[clinic start generated code]*/

static PyObject *
os_process_cpu_count_impl(PyObject *module)
/*[clinic end generated code: output=dc750a336e010b50 input=b9b63acbae0dbe45]*/
{
#if defined(HAVE_SCHED_H) && defined(HAVE_SCHED_SETAFFINITY)
int ncpus = NCPUS_START;
cpu_set_t *mask;
size_t setsize;

while (1) {
setsize = CPU_ALLOC_SIZE(ncpus);
mask = CPU_ALLOC(ncpus);
if (mask == NULL) {
return PyErr_NoMemory();
}
if (sched_getaffinity(0, setsize, mask) == 0) {
break;
}
CPU_FREE(mask);
if (errno != EINVAL) {
return posix_error();
}
if (ncpus > INT_MAX / 2) {
PyErr_SetString(PyExc_OverflowError,
"could not allocate a large enough CPU set");
return NULL;
}
ncpus *= 2;
}

int ncpu = CPU_COUNT_S(setsize, mask);
CPU_FREE(mask);
return PyLong_FromLong(ncpu);
#else
return os_cpu_count_impl(NULL);
#endif
}


Expand Down Expand Up @@ -15972,6 +16030,7 @@ static PyMethodDef posix_methods[] = {

OS_GET_TERMINAL_SIZE_METHODDEF
OS_CPU_COUNT_METHODDEF
OS_PROCESS_CPU_COUNT_METHODDEF
OS_GET_INHERITABLE_METHODDEF
OS_SET_INHERITABLE_METHODDEF
OS_GET_HANDLE_INHERITABLE_METHODDEF
Expand Down

0 comments on commit e3554fa

Please sign in to comment.