use std::collections::HashSet;
use compio_log::*;
pub fn bind_to_cpu_set(cpus: &HashSet<usize>) {
if cpus.is_empty() {
return;
}
let Some(ids) = available_cpus() else {
return;
};
match (ids.iter().max(), cpus.iter().max()) {
(Some(max_id), Some(max_cpu)) if *max_cpu > *max_id => {
error!("CPU ID: {max_cpu} exceeds maximum available CPU ID: {max_id}");
}
_ => {}
}
let cpu_set = ids.intersection(cpus).copied();
set_affinity(cpu_set);
}
#[cfg(any(target_os = "linux", target_os = "android"))]
fn available_cpus() -> Option<HashSet<usize>> {
use std::mem;
let set = unsafe {
let mut set: libc::cpu_set_t = mem::zeroed();
if libc::sched_getaffinity(0, mem::size_of::<libc::cpu_set_t>(), &mut set) != 0 {
return None;
}
set
};
let cpu_set_size = {
#[cfg(target_os = "linux")]
{
libc::CPU_SETSIZE as usize
}
#[cfg(target_os = "android")]
{
libc::CPU_SETSIZE
}
};
Some(
(0..cpu_set_size)
.filter(|&i| unsafe { libc::CPU_ISSET(i, &set) })
.collect(),
)
}
#[cfg(any(target_os = "linux", target_os = "android"))]
fn set_affinity(cpus: impl Iterator<Item = usize>) {
use std::{io, mem};
let res = unsafe {
let mut set: libc::cpu_set_t = mem::zeroed();
for cpu in cpus {
libc::CPU_SET(cpu, &mut set);
}
libc::sched_setaffinity(0, mem::size_of::<libc::cpu_set_t>(), &set)
};
if res != 0 {
warn!(
"cannot set CPU affinity for current thread: {}",
io::Error::last_os_error()
);
}
}
#[cfg(target_os = "freebsd")]
fn available_cpus() -> Option<HashSet<usize>> {
use std::mem;
let set = unsafe {
let mut set: libc::cpuset_t = mem::zeroed();
let res = libc::cpuset_getaffinity(
libc::CPU_LEVEL_WHICH,
libc::CPU_WHICH_TID,
-1,
mem::size_of::<libc::cpuset_t>(),
&mut set,
);
if res != 0 {
return None;
}
set
};
Some(
(0..libc::CPU_SETSIZE as usize)
.filter(|&i| unsafe { libc::CPU_ISSET(i, &set) })
.collect(),
)
}
#[cfg(target_os = "freebsd")]
fn set_affinity(cpus: impl Iterator<Item = usize>) {
use std::{io, mem};
let res = unsafe {
let mut set: libc::cpuset_t = mem::zeroed();
for cpu in cpus {
libc::CPU_SET(cpu, &mut set);
}
libc::cpuset_setaffinity(
libc::CPU_LEVEL_WHICH,
libc::CPU_WHICH_TID,
-1,
mem::size_of::<libc::cpuset_t>(),
&set,
)
};
if res != 0 {
warn!(
"cannot set CPU affinity for current thread: {}",
io::Error::last_os_error()
);
}
}
#[cfg(target_os = "netbsd")]
fn available_cpus() -> Option<HashSet<usize>> {
unsafe {
let set = libc::_cpuset_create();
if set.is_null() {
return None;
}
let res = libc::pthread_getaffinity_np(libc::pthread_self(), libc::_cpuset_size(set), set);
if res != 0 {
libc::_cpuset_destroy(set);
return None;
}
let bits = libc::_cpuset_size(set) * 8;
let cpus = (0..bits)
.filter(|&i| libc::_cpuset_isset(i as libc::c_ulong, set) > 0)
.collect();
libc::_cpuset_destroy(set);
Some(cpus)
}
}
#[cfg(target_os = "netbsd")]
fn set_affinity(cpus: impl Iterator<Item = usize>) {
use std::io;
let res = unsafe {
let set = libc::_cpuset_create();
if set.is_null() {
warn!("cannot allocate cpuset for current thread");
return;
}
for cpu in cpus {
libc::_cpuset_set(cpu as libc::c_ulong, set);
}
let res = libc::pthread_setaffinity_np(libc::pthread_self(), libc::_cpuset_size(set), set);
libc::_cpuset_destroy(set);
res
};
if res != 0 {
warn!(
"cannot set CPU affinity for current thread: {}",
io::Error::from_raw_os_error(res)
);
}
}
#[cfg(windows)]
fn available_cpus() -> Option<HashSet<usize>> {
use windows_sys::Win32::System::Threading::{GetCurrentProcess, GetProcessAffinityMask};
let mut process_mask: usize = 0;
let mut system_mask: usize = 0;
let res =
unsafe { GetProcessAffinityMask(GetCurrentProcess(), &mut process_mask, &mut system_mask) };
if res == 0 {
return None;
}
Some(
(0..usize::BITS as usize)
.filter(|&i| process_mask & (1usize << i) != 0)
.collect(),
)
}
#[cfg(windows)]
fn set_affinity(cpus: impl Iterator<Item = usize>) {
use std::io;
use windows_sys::Win32::System::Threading::{GetCurrentThread, SetThreadAffinityMask};
let bits = usize::BITS as usize;
let mut mask: usize = 0;
for cpu in cpus {
if cpu >= bits {
warn!("CPU {cpu} exceeds the affinity mask width ({bits}); ignoring");
continue;
}
mask |= 1usize << cpu;
}
if mask == 0 {
return;
}
let res = unsafe { SetThreadAffinityMask(GetCurrentThread(), mask) };
if res == 0 {
warn!(
"cannot set CPU affinity for current thread: {}",
io::Error::last_os_error()
);
}
}
#[cfg(target_os = "macos")]
fn available_cpus() -> Option<HashSet<usize>> {
let n = std::thread::available_parallelism().ok()?.get();
Some((0..n).collect())
}
#[cfg(target_os = "macos")]
fn set_affinity(cpus: impl Iterator<Item = usize>) {
let mut cpus = cpus;
let Some(tag) = cpus.next() else {
return;
};
if cpus.next().is_some() {
warn!("only setting affinity to the first CPU, ignoring extra provided on this platform");
}
let mut info = libc::thread_affinity_policy_data_t {
affinity_tag: tag as libc::integer_t,
};
let res = unsafe {
libc::thread_policy_set(
libc::pthread_mach_thread_np(libc::pthread_self()),
libc::THREAD_AFFINITY_POLICY as libc::thread_policy_flavor_t,
(&mut info as *mut libc::thread_affinity_policy_data_t).cast(),
libc::THREAD_AFFINITY_POLICY_COUNT,
)
};
if res != 0 {
warn!("cannot set CPU affinity for current thread: kern_return_t {res}");
}
}
#[cfg(not(any(
target_os = "linux",
target_os = "android",
target_os = "freebsd",
target_os = "netbsd",
target_os = "macos",
windows,
)))]
fn available_cpus() -> Option<HashSet<usize>> {
None
}
#[cfg(not(any(
target_os = "linux",
target_os = "android",
target_os = "freebsd",
target_os = "netbsd",
target_os = "macos",
windows,
)))]
fn set_affinity(_cpus: impl Iterator<Item = usize>) {
warn!("ignore setting CPU affinity for current thread: not supported on this platform");
}
#[cfg(all(
test,
any(
target_os = "linux",
target_os = "android",
target_os = "freebsd",
target_os = "netbsd"
)
))]
mod tests {
use std::collections::HashSet;
use super::{available_cpus, bind_to_cpu_set};
#[test]
fn available_cpus_is_nonempty() {
let cpus = available_cpus().expect("thread affinity must be available");
assert!(!cpus.is_empty());
}
#[test]
fn binds_every_requested_cpu() {
let available = available_cpus().expect("thread affinity must be available");
let want: HashSet<usize> = available.into_iter().take(2).collect();
bind_to_cpu_set(&want);
assert_eq!(available_cpus().unwrap(), want);
}
#[test]
fn ignores_nonexistent_cpus() {
let available = available_cpus().expect("thread affinity must be available");
let max = *available.iter().max().unwrap();
let valid = *available.iter().min().unwrap();
let want = HashSet::from([valid, max + 1000]);
bind_to_cpu_set(&want);
assert_eq!(available_cpus().unwrap(), HashSet::from([valid]));
}
}
#[cfg(all(test, windows))]
mod tests {
use std::collections::HashSet;
use windows_sys::Win32::System::Threading::{GetCurrentThread, SetThreadAffinityMask};
use super::{available_cpus, bind_to_cpu_set};
#[test]
fn available_cpus_is_nonempty() {
let cpus = available_cpus().expect("process affinity must be available");
assert!(!cpus.is_empty());
}
#[test]
fn binds_every_requested_cpu() {
let available = available_cpus().expect("process affinity must be available");
let want: HashSet<usize> = available.into_iter().take(2).collect();
bind_to_cpu_set(&want);
let want_mask = want.iter().fold(0usize, |m, &c| m | (1usize << c));
let active = unsafe { SetThreadAffinityMask(GetCurrentThread(), want_mask) };
assert_eq!(active, want_mask);
}
}
#[cfg(all(test, target_os = "macos"))]
mod tests {
use std::collections::HashSet;
use super::{available_cpus, bind_to_cpu_set};
#[test]
fn available_cpus_is_nonempty() {
let cpus = available_cpus().expect("logical CPU count must be available");
assert!(!cpus.is_empty());
}
#[test]
fn bind_runs_cleanly() {
let available = available_cpus().unwrap();
let want: HashSet<usize> = available.into_iter().take(2).collect();
bind_to_cpu_set(&want);
}
}