Skip to content

Commit f2342c2

Browse files
Add Crystal::System.effective_cpu_count (#16148)
The method is meant to return how many logical CPUs are actually available to the process versus the total number of CPUs from `System.cpu_count`. - Linux: uses `sched_getaffinity` syscall - Darwin / DragonflyBSD: _unsupported_ - FreeBSD: uses `cpuset_getaffinity` - NetBSD / OpenBSD: uses `sysctl(CTL_HW, HW_NCPUONLINE` - Solaris: _unknown_ - Windows: uses `GetProcessAffinityMask` Co-authored-by: Quinton Miller <[email protected]>
1 parent 23e719a commit f2342c2

File tree

13 files changed

+102
-8
lines changed

13 files changed

+102
-8
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
require "spec"
2+
3+
describe Crystal::System do
4+
it "#effective_cpu_count" do
5+
# smoke test: must compile and must run
6+
Crystal::System.effective_cpu_count
7+
end
8+
end

src/crystal/system.cr

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,29 @@ module Crystal::System
33
# Returns the hostname
44
# def self.hostname
55

6-
# Returns the number of logical processors available to the system.
7-
#
6+
# Returns the number of logical processors available to the system. Returns -1
7+
# on errors or when unknown.
88
# def self.cpu_count
9+
10+
# Returns the number of logical processors available to the process. Should be
11+
# less than or equal to `.cpu_count`. Returns -1 on errors or when unknown.
12+
def self.effective_cpu_count
13+
-1
14+
end
915
end
1016

1117
{% if flag?(:wasi) %}
1218
require "./system/wasi/hostname"
1319
require "./system/wasi/cpucount"
1420
{% elsif flag?(:unix) %}
1521
require "./system/unix/hostname"
16-
1722
{% if flag?(:bsd) %}
1823
require "./system/unix/sysctl_cpucount"
1924
{% else %}
2025
require "./system/unix/sysconf_cpucount"
26+
{% if flag?(:linux) %}
27+
require "./system/unix/linux_cpucount"
28+
{% end %}
2129
{% end %}
2230
{% elsif flag?(:win32) %}
2331
require "./system/win32/hostname"
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{% skip_file unless flag?(:linux) %}
2+
3+
require "./syscall"
4+
5+
module Crystal::System
6+
def self.effective_cpu_count
7+
{% unless flag?(:interpreted) %}
8+
# we use the syscall because it returns the number of bytes to check in
9+
# the set, while glibc always returns 0 and would require to zero the
10+
# buffer and check every byte
11+
set = uninitialized UInt8[8192] # allows up to 65536 logical cpus
12+
byte_count = Syscall.sched_getaffinity(0, LibC::SizeT.new(8192), set.to_unsafe)
13+
if byte_count > 0
14+
count = set.to_slice[0, byte_count].sum(&.popcount)
15+
return count if count > 0
16+
end
17+
{% end %}
18+
19+
-1
20+
end
21+
end

src/crystal/system/unix/syscall.cr

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ module Crystal::System::Syscall
77
GRND_NONBLOCK = 1u32
88

99
::Syscall.def_syscall getrandom, LibC::SSizeT, buf : UInt8*, buflen : LibC::SizeT, flags : UInt32
10+
::Syscall.def_syscall sched_getaffinity, LibC::Int, pid : LibC::PidT, cpusetsize : LibC::SizeT, mask : Pointer(UInt8)
1011
end

src/crystal/system/unix/sysctl_cpucount.cr

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,47 @@
22

33
require "c/sysctl"
44

5+
{% if flag?(:freebsd) %}
6+
require "c/sys/cpuset"
7+
{% end %}
8+
59
module Crystal::System
610
def self.cpu_count
711
mib = Int32[LibC::CTL_HW, LibC::HW_NCPU]
812
ncpus = 0
9-
size = sizeof(Int32).to_u64
13+
size = LibC::SizeT.new(sizeof(Int32))
1014

1115
if LibC.sysctl(mib, 2, pointerof(ncpus), pointerof(size), nil, 0) == 0
1216
ncpus
1317
else
1418
-1
1519
end
1620
end
21+
22+
def self.effective_cpu_count
23+
{% if flag?(:freebsd) %}
24+
buffer = uninitialized UInt8[8192]
25+
maxcpus = 0
26+
size = LibC::SizeT.new(sizeof(Int32))
27+
28+
if LibC.sysctlbyname("kern.smp.maxcpus", pointerof(maxcpus), pointerof(size), nil, 0) == 0
29+
len = ((maxcpus + 7) // 8).clamp(..buffer.size)
30+
set = buffer.to_slice[0, len]
31+
32+
if LibC.cpuset_getaffinity(LibC::CPU_LEVEL_WHICH, LibC::CPU_WHICH_PID, -1, len, set) == 0
33+
return set.sum(&.popcount)
34+
end
35+
end
36+
{% elsif flag?(:netbsd) || flag?(:openbsd) %}
37+
mib = Int32[LibC::CTL_HW, LibC::HW_NCPUONLINE]
38+
ncpus = 0
39+
size = LibC::SizeT.new(sizeof(Int32))
40+
41+
if LibC.sysctl(mib, 2, pointerof(ncpus), pointerof(size), nil, 0) == 0
42+
return ncpus
43+
end
44+
{% end %}
45+
46+
-1
47+
end
1748
end

src/crystal/system/wasi/cpucount.cr

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,8 @@ module Crystal::System
33
# TODO: There isn't a good way to get the number of CPUs on WebAssembly
44
1
55
end
6+
7+
def self.effective_cpu_count
8+
-1
9+
end
610
end

src/crystal/system/win32/cpucount.cr

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,12 @@ module Crystal::System
55
LibC.GetNativeSystemInfo(out system_info)
66
system_info.dwNumberOfProcessors
77
end
8+
9+
def self.effective_cpu_count
10+
if LibC.GetProcessAffinityMask(LibC.GetCurrentProcess, out process_affinity, out _) == 0
11+
-1
12+
else
13+
process_affinity.popcount
14+
end
15+
end
816
end
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
lib LibC
2+
CPU_LEVEL_WHICH = 3
3+
CPU_WHICH_PID = 2
4+
5+
fun cpuset_getaffinity(Int32, Int32, IdT, SizeT, UInt8*)
6+
end

src/lib_c/x86_64-freebsd/c/sysctl.cr

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ lib LibC
77
PATH_MAX = 1024
88

99
fun sysctl(name : Int*, namelen : UInt, oldp : Void*, oldlenp : SizeT*, newp : Void*, newlen : SizeT) : Int
10+
fun sysctlbyname(name : UInt8*, oldp : Void*, oldlenp : SizeT*, newp : Void*, newlen : SizeT) : Int
1011
end

src/lib_c/x86_64-netbsd/c/sysctl.cr

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
lib LibC
2-
CTL_HW = 6
3-
HW_NCPU = 3
2+
CTL_HW = 6
3+
HW_NCPU = 3
4+
HW_NCPUONLINE = 16
45

56
CTL_KERN = 1
67
KERN_PROC = 14

0 commit comments

Comments
 (0)