Skip to content

Commit 7832caa

Browse files
committed
gh-109649: Add os.process_cpu_count() function
* Fix test_posix.test_sched_getaffinity(): restore the old CPU mask when the test completes! * Doc: Specify that os.cpu_count() counts *logicial* CPUs.
1 parent 9c73a9a commit 7832caa

File tree

7 files changed

+169
-45
lines changed

7 files changed

+169
-45
lines changed

Doc/library/os.rst

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5183,12 +5183,10 @@ Miscellaneous System Information
51835183

51845184
.. function:: cpu_count()
51855185

5186-
Return the number of CPUs in the system. Returns ``None`` if undetermined.
5187-
5188-
This number is not equivalent to the number of CPUs the current process can
5189-
use. The number of usable CPUs can be obtained with
5190-
``len(os.sched_getaffinity(0))``
5186+
Return the number of logical CPUs in the system. Returns ``None`` if
5187+
undetermined.
51915188

5189+
See also the :func:`process_cpu_count` function.
51925190

51935191
.. versionadded:: 3.4
51945192

@@ -5202,6 +5200,17 @@ Miscellaneous System Information
52025200
.. availability:: Unix.
52035201

52045202

5203+
.. function:: process_cpu_count()
5204+
5205+
Get the number of logical CPUs usable by the current process. Returns
5206+
``None`` if undetermined. It can be less than :func:`cpu_count` depending on
5207+
the process affinity.
5208+
5209+
See also the :func:`cpu_count` function.
5210+
5211+
.. versionadded:: 3.13
5212+
5213+
52055214
.. function:: sysconf(name, /)
52065215

52075216
Return integer-valued system configuration values. If the configuration value

Doc/whatsnew/3.13.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,13 @@ opcode
163163
documented or exposed through ``dis``, and were not intended to be
164164
used externally.
165165

166+
os
167+
--
168+
169+
* Add :func:`os.process_cpu_count` function to get the number of logical CPUs
170+
usable by the current process.
171+
(Contributed by Victor Stinner in :gh:`109649`.)
172+
166173
pathlib
167174
-------
168175

Lib/test/test_os.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3996,14 +3996,42 @@ def test_oserror_filename(self):
39963996
self.fail(f"No exception thrown by {func}")
39973997

39983998
class CPUCountTests(unittest.TestCase):
3999+
def check_cpu_count(self, cpus):
4000+
if cpus is None:
4001+
self.skipTest("Could not determine the number of CPUs")
4002+
4003+
self.assertIsInstance(cpus, int)
4004+
self.assertGreater(cpus, 0)
4005+
39994006
def test_cpu_count(self):
40004007
cpus = os.cpu_count()
4001-
if cpus is not None:
4002-
self.assertIsInstance(cpus, int)
4003-
self.assertGreater(cpus, 0)
4004-
else:
4008+
self.check_cpu_count(cpus)
4009+
4010+
def test_process_cpu_count(self):
4011+
cpus = os.process_cpu_count()
4012+
self.assertLessEqual(cpus, os.cpu_count())
4013+
self.check_cpu_count(cpus)
4014+
4015+
@unittest.skipUnless(hasattr(os, 'sched_setaffinity'),
4016+
"don't have sched affinity support")
4017+
def test_process_cpu_count_affinity(self):
4018+
ncpu = os.cpu_count()
4019+
if ncpu is None:
40054020
self.skipTest("Could not determine the number of CPUs")
40064021

4022+
# Disable one CPU
4023+
mask = os.sched_getaffinity(0)
4024+
if len(mask) <= 1:
4025+
self.skipTest(f"sched_getaffinity() returns less than "
4026+
f"2 CPUs: {sorted(mask)}")
4027+
self.addCleanup(os.sched_setaffinity, 0, list(mask))
4028+
mask.pop()
4029+
os.sched_setaffinity(0, mask)
4030+
4031+
# test process_cpu_count()
4032+
affinity = os.process_cpu_count()
4033+
self.assertEqual(affinity, ncpu - 1)
4034+
40074035

40084036
# FD inheritance check is only useful for systems with process support.
40094037
@support.requires_subprocess()

Lib/test/test_posix.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1205,6 +1205,7 @@ def test_sched_getaffinity(self):
12051205
@requires_sched_affinity
12061206
def test_sched_setaffinity(self):
12071207
mask = posix.sched_getaffinity(0)
1208+
self.addCleanup(posix.sched_setaffinity, 0, list(mask))
12081209
if len(mask) > 1:
12091210
# Empty masks are forbidden
12101211
mask.pop()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add :func:`os.process_cpu_count` function to get the number of logical CPUs
2+
usable by the current process. Patch by Victor Stinner.

Modules/clinic/posixmodule.c.h

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

Modules/posixmodule.c

Lines changed: 90 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8134,39 +8134,45 @@ static PyObject *
81348134
os_sched_getaffinity_impl(PyObject *module, pid_t pid)
81358135
/*[clinic end generated code: output=f726f2c193c17a4f input=983ce7cb4a565980]*/
81368136
{
8137-
int cpu, ncpus, count;
8137+
int ncpus = NCPUS_START;
81388138
size_t setsize;
8139-
cpu_set_t *mask = NULL;
8140-
PyObject *res = NULL;
8139+
cpu_set_t *mask;
81418140

8142-
ncpus = NCPUS_START;
81438141
while (1) {
81448142
setsize = CPU_ALLOC_SIZE(ncpus);
81458143
mask = CPU_ALLOC(ncpus);
8146-
if (mask == NULL)
8144+
if (mask == NULL) {
81478145
return PyErr_NoMemory();
8148-
if (sched_getaffinity(pid, setsize, mask) == 0)
8146+
}
8147+
if (sched_getaffinity(pid, setsize, mask) == 0) {
81498148
break;
8149+
}
81508150
CPU_FREE(mask);
8151-
if (errno != EINVAL)
8151+
if (errno != EINVAL) {
81528152
return posix_error();
8153+
}
81538154
if (ncpus > INT_MAX / 2) {
8154-
PyErr_SetString(PyExc_OverflowError, "could not allocate "
8155-
"a large enough CPU set");
8155+
PyErr_SetString(PyExc_OverflowError,
8156+
"could not allocate a large enough CPU set");
81568157
return NULL;
81578158
}
8158-
ncpus = ncpus * 2;
8159+
ncpus *= 2;
81598160
}
81608161

8161-
res = PySet_New(NULL);
8162-
if (res == NULL)
8162+
PyObject *res = PySet_New(NULL);
8163+
if (res == NULL) {
81638164
goto error;
8164-
for (cpu = 0, count = CPU_COUNT_S(setsize, mask); count; cpu++) {
8165+
}
8166+
8167+
int cpu = 0;
8168+
int count = CPU_COUNT_S(setsize, mask);
8169+
for (; count; cpu++) {
81658170
if (CPU_ISSET_S(cpu, setsize, mask)) {
81668171
PyObject *cpu_num = PyLong_FromLong(cpu);
81678172
--count;
8168-
if (cpu_num == NULL)
8173+
if (cpu_num == NULL) {
81698174
goto error;
8175+
}
81708176
if (PySet_Add(res, cpu_num)) {
81718177
Py_DECREF(cpu_num);
81728178
goto error;
@@ -8178,12 +8184,12 @@ os_sched_getaffinity_impl(PyObject *module, pid_t pid)
81788184
return res;
81798185

81808186
error:
8181-
if (mask)
8187+
if (mask) {
81828188
CPU_FREE(mask);
8189+
}
81838190
Py_XDECREF(res);
81848191
return NULL;
81858192
}
8186-
81878193
#endif /* HAVE_SCHED_SETAFFINITY */
81888194

81898195
#endif /* HAVE_SCHED_H */
@@ -14334,44 +14340,96 @@ os_get_terminal_size_impl(PyObject *module, int fd)
1433414340
/*[clinic input]
1433514341
os.cpu_count
1433614342
14337-
Return the number of CPUs in the system; return None if indeterminable.
14343+
Return the number of logical CPUs in the system.
1433814344
14339-
This number is not equivalent to the number of CPUs the current process can
14340-
use. The number of usable CPUs can be obtained with
14341-
``len(os.sched_getaffinity(0))``
14345+
Return None if indeterminable.
1434214346
[clinic start generated code]*/
1434314347

1434414348
static PyObject *
1434514349
os_cpu_count_impl(PyObject *module)
14346-
/*[clinic end generated code: output=5fc29463c3936a9c input=e7c8f4ba6dbbadd3]*/
14350+
/*[clinic end generated code: output=5fc29463c3936a9c input=ba2f6f8980a0e2eb]*/
1434714351
{
14348-
int ncpu = 0;
14352+
int ncpu;
1434914353
#ifdef MS_WINDOWS
14350-
#ifdef MS_WINDOWS_DESKTOP
14354+
# ifdef MS_WINDOWS_DESKTOP
1435114355
ncpu = GetActiveProcessorCount(ALL_PROCESSOR_GROUPS);
14352-
#endif
14356+
# else
14357+
ncpu = 0;
14358+
# endif
14359+
1435314360
#elif defined(__hpux)
1435414361
ncpu = mpctl(MPC_GETNUMSPUS, NULL, NULL);
14362+
1435514363
#elif defined(HAVE_SYSCONF) && defined(_SC_NPROCESSORS_ONLN)
1435614364
ncpu = sysconf(_SC_NPROCESSORS_ONLN);
14365+
1435714366
#elif defined(__VXWORKS__)
1435814367
ncpu = _Py_popcount32(vxCpuEnabledGet());
14368+
1435914369
#elif defined(__DragonFly__) || \
1436014370
defined(__OpenBSD__) || \
1436114371
defined(__FreeBSD__) || \
1436214372
defined(__NetBSD__) || \
1436314373
defined(__APPLE__)
14364-
int mib[2];
14374+
ncpu = 0;
1436514375
size_t len = sizeof(ncpu);
14366-
mib[0] = CTL_HW;
14367-
mib[1] = HW_NCPU;
14368-
if (sysctl(mib, 2, &ncpu, &len, NULL, 0) != 0)
14376+
int mib[2] = {CTL_HW, HW_NCPU};
14377+
if (sysctl(mib, 2, &ncpu, &len, NULL, 0) != 0) {
1436914378
ncpu = 0;
14379+
}
1437014380
#endif
14371-
if (ncpu >= 1)
14372-
return PyLong_FromLong(ncpu);
14373-
else
14381+
14382+
if (ncpu < 1) {
1437414383
Py_RETURN_NONE;
14384+
}
14385+
return PyLong_FromLong(ncpu);
14386+
}
14387+
14388+
14389+
/*[clinic input]
14390+
os.process_cpu_count
14391+
14392+
Get the number of logical CPUs usable by the current process.
14393+
14394+
Return None if indeterminable.
14395+
[clinic start generated code]*/
14396+
14397+
static PyObject *
14398+
os_process_cpu_count_impl(PyObject *module)
14399+
/*[clinic end generated code: output=dc750a336e010b50 input=b9b63acbae0dbe45]*/
14400+
{
14401+
#if defined(HAVE_SCHED_H) && defined(HAVE_SCHED_SETAFFINITY)
14402+
int ncpus = NCPUS_START;
14403+
cpu_set_t *mask;
14404+
size_t setsize;
14405+
14406+
while (1) {
14407+
setsize = CPU_ALLOC_SIZE(ncpus);
14408+
mask = CPU_ALLOC(ncpus);
14409+
if (mask == NULL) {
14410+
return PyErr_NoMemory();
14411+
}
14412+
if (sched_getaffinity(0, setsize, mask) == 0) {
14413+
break;
14414+
}
14415+
CPU_FREE(mask);
14416+
if (errno != EINVAL) {
14417+
return posix_error();
14418+
}
14419+
if (ncpus > INT_MAX / 2) {
14420+
PyErr_SetString(PyExc_OverflowError,
14421+
"could not allocate a large enough CPU set");
14422+
return NULL;
14423+
}
14424+
ncpus *= 2;
14425+
}
14426+
14427+
int ncpu = CPU_COUNT_S(setsize, mask);
14428+
CPU_FREE(mask);
14429+
return PyLong_FromLong(ncpu);
14430+
#else
14431+
return os_cpu_count_impl(NULL);
14432+
#endif
1437514433
}
1437614434

1437714435

@@ -15968,6 +16026,7 @@ static PyMethodDef posix_methods[] = {
1596816026

1596916027
OS_GET_TERMINAL_SIZE_METHODDEF
1597016028
OS_CPU_COUNT_METHODDEF
16029+
OS_PROCESS_CPU_COUNT_METHODDEF
1597116030
OS_GET_INHERITABLE_METHODDEF
1597216031
OS_SET_INHERITABLE_METHODDEF
1597316032
OS_GET_HANDLE_INHERITABLE_METHODDEF

0 commit comments

Comments
 (0)