|
#!/usr/bin/env python3 |
|
# /// script |
|
# requires-python = ">=3.9" |
|
# dependencies = [] |
|
# /// |
|
|
|
""" |
|
Print heterogeneous CPU core counts. |
|
For macOS, Linux, and Windows. |
|
|
|
python3 cpu_hetero.py |
|
""" |
|
|
|
from __future__ import annotations |
|
|
|
import platform |
|
from collections import defaultdict |
|
from pathlib import Path |
|
from pprint import pprint |
|
|
|
|
|
def macos() -> dict[str, int]: |
|
import subprocess |
|
|
|
n = int(subprocess.check_output(["sysctl", "-n", "hw.nperflevels"], text=True)) |
|
counts: dict[str, int] = {} |
|
for i in range(n): |
|
count = int(subprocess.check_output( |
|
["sysctl", "-n", f"hw.perflevel{i}.physicalcpu"], text=True |
|
)) |
|
name = subprocess.check_output( |
|
["sysctl", "-n", f"hw.perflevel{i}.name"], text=True |
|
).strip() |
|
counts[name or str(i)] = count |
|
|
|
return counts |
|
|
|
|
|
def _expand(spec: str) -> list[int]: |
|
out: list[int] = [] |
|
for part in spec.strip().split(","): |
|
if not part: |
|
continue |
|
if "-" in part: |
|
a, b = part.split("-", 1) |
|
out.extend(range(int(a), int(b) + 1)) |
|
else: |
|
out.append(int(part)) |
|
return out |
|
|
|
|
|
def _physical(cpus: list[int]) -> int: |
|
seen = set() |
|
for cpu in cpus: |
|
base = Path(f"/sys/devices/system/cpu/cpu{cpu}/topology") |
|
sib = base / "core_cpus_list" |
|
if not sib.exists(): |
|
sib = base / "thread_siblings_list" |
|
key = sib.read_text().strip() if sib.exists() else str(cpu) |
|
seen.add(key) |
|
return len(seen) |
|
|
|
|
|
def _cpu_count(spec: str) -> int: |
|
cpus = _expand(spec) |
|
return _physical(cpus) |
|
|
|
|
|
def linux() -> dict[str, int]: |
|
counts: dict[str, int] = {} |
|
names = { |
|
"cpu_core": "Performance", |
|
"cpu_atom": "Efficiency", |
|
} |
|
for path in sorted(Path("/sys/devices/system/cpu").glob("cpu_*/cpus")): |
|
kind = path.parent.name |
|
counts[names.get(kind, kind)] = _cpu_count(path.read_text()) |
|
|
|
if counts: |
|
return counts |
|
|
|
# ARM / capacity-aware kernels |
|
by_cap: dict[int, list[int]] = defaultdict(list) |
|
for d in sorted(Path("/sys/devices/system/cpu").glob("cpu[0-9]*")): |
|
cap = d / "cpu_capacity" |
|
if not cap.exists(): |
|
continue |
|
by_cap[int(cap.read_text())].append(int(d.name[3:])) |
|
|
|
if by_cap: |
|
return {str(i): _physical(by_cap[cap]) for i, cap in enumerate(sorted(by_cap, reverse=True))} |
|
|
|
online = Path("/sys/devices/system/cpu/online") |
|
if online.exists(): |
|
return {"0": _physical(_expand(online.read_text()))} |
|
return {"0": 0} |
|
|
|
|
|
def windows() -> dict[str, int]: |
|
import ctypes |
|
from ctypes import wintypes |
|
|
|
RelationProcessorCore = 0 |
|
ERROR_INSUFFICIENT_BUFFER = 122 |
|
LTP_PC_SMT = 0x1 |
|
|
|
class GROUP_AFFINITY(ctypes.Structure): |
|
_fields_ = [ |
|
("Mask", ctypes.c_uint64), |
|
("Group", wintypes.WORD), |
|
("Reserved", wintypes.WORD * 3), |
|
] |
|
|
|
class PROCESSOR_RELATIONSHIP(ctypes.Structure): |
|
_fields_ = [ |
|
("Flags", ctypes.c_ubyte), |
|
("EfficiencyClass", ctypes.c_ubyte), |
|
("Reserved", ctypes.c_ubyte * 20), |
|
("GroupCount", wintypes.WORD), |
|
("GroupMask", GROUP_AFFINITY * 1), |
|
] |
|
|
|
class SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX(ctypes.Structure): |
|
_fields_ = [ |
|
("Relationship", wintypes.DWORD), |
|
("Size", wintypes.DWORD), |
|
("Processor", PROCESSOR_RELATIONSHIP), |
|
] |
|
|
|
windll = getattr(ctypes, "WinDLL") |
|
get_last_error = getattr(ctypes, "get_last_error") |
|
win_error = getattr(ctypes, "WinError") |
|
|
|
k32 = windll("kernel32", use_last_error=True) |
|
fn = k32.GetLogicalProcessorInformationEx |
|
fn.argtypes = [wintypes.DWORD, ctypes.c_void_p, ctypes.POINTER(wintypes.DWORD)] |
|
fn.restype = wintypes.BOOL |
|
|
|
size = wintypes.DWORD(0) |
|
if fn(RelationProcessorCore, None, ctypes.byref(size)): |
|
raise OSError("unexpected success with null buffer") |
|
if get_last_error() != ERROR_INSUFFICIENT_BUFFER: |
|
raise win_error() |
|
|
|
buf = ctypes.create_string_buffer(size.value) |
|
if not fn(RelationProcessorCore, buf, ctypes.byref(size)): |
|
raise win_error() |
|
|
|
by_class = defaultdict(lambda: {"physical": 0, "logical": 0, "smt": 0}) |
|
offset = 0 |
|
while offset < size.value: |
|
info = SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX.from_buffer(buf, offset) |
|
proc = info.Processor |
|
group_mask_offset = ( |
|
offset |
|
+ SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX.Processor.offset |
|
+ PROCESSOR_RELATIONSHIP.GroupMask.offset |
|
) |
|
bits = sum( |
|
bin(GROUP_AFFINITY.from_buffer( |
|
buf, group_mask_offset + group * ctypes.sizeof(GROUP_AFFINITY) |
|
).Mask).count("1") |
|
for group in range(proc.GroupCount) |
|
) |
|
rec = by_class[proc.EfficiencyClass] |
|
rec["physical"] += 1 |
|
rec["logical"] += bits |
|
if proc.Flags & LTP_PC_SMT: |
|
rec["smt"] += 1 |
|
offset += info.Size |
|
|
|
classes = sorted(by_class) |
|
return {str(cls): by_class[cls]["physical"] for cls in classes} |
|
|
|
|
|
if __name__ == "__main__": |
|
osname = platform.system() |
|
if osname == "Darwin": |
|
pprint(macos()) |
|
elif osname == "Linux": |
|
pprint(linux()) |
|
elif osname == "Windows": |
|
pprint(windows()) |
|
else: |
|
raise SystemExit(f"Unsupported OS: {osname}") |