enum memory_domain_id_t A machine is described along two independent axes. A compute domain is a bindable group of cores sharing a Quality-of-Service class and locality — it is what a pool spawns onto, and the index a worker callback receives. A memory domain is a bank of memory with its own capacity and access cost — it is what the allocator targets, may be cpuless like a CXL expander, and may serve several compute domains at once. The axes stay separate because they don’t line up: performance and efficiency cores often share one memory controller, and equally-fast cores are often split across cache clusters or sockets.
Each axis carries a level, a dense ordinal grouping domains of like performance.
Levels can be fewer than domains, since several domains may share one, and the two axes count in opposite directions: compute levels grow with performance, memory levels grow with distance, placing HBM below DDR and CXL above.
Two objects split the answers by provenance, not by axis: a Topology holds what the platform declares — domains, cores, QoS classes, volumes, on both axes — while a Fabric holds what ForkUnion observes: per-edge latencies, bandwidths, and distances, and the per-medium tiers derived from them.
The pipeline is harvest all the way down: a Topology harvests the declared structure from the OS and stays immutable, a pool spawns on it, and a Fabric then harvests the observed performance from the silicon through that pool’s pinned workers, pointer-chasing and streaming every reachable edge.
A memory level is a property of the medium, independent of the querying core: it keys on the best bandwidth any initiator sustains to the pool, ties split by the best latency, so a 3 TB/s HBM pool outranks DDR even at equal latency.
| Compute axis | Memory axis | |
|---|---|---|
| Count domains | compute_domains_count | memory_domains_count |
| Count levels | compute_levels_count | memory_levels_count |
| Level of a domain | compute_level_in | memory_level_in |
| Faster means | higher | lower |
Names are spelled here as in Rust, which Mojo matches; C prefixes them with fu_, and Zig spells them in camelCase.
Rust, Zig, and Mojo all carry the domain coordinates as distinct types — ComputeDomain, MemoryDomain, and MemoryDomainId — while C passes plain indices.
Domain counts and compute levels answer from the Topology; memory levels and the per-edge magnitudes answer from a harvested Fabric, which C prefixes with fu_fabric_.
Beyond the level ordinals, two magnitudes describe a compute domain, both best-effort. compute_capacity_in forwards the kernel’s own rating of a core, read from /sys/devices/system/cpu/cpuN/cpu_capacity, normalized so the fastest core on the machine reads 1024.
Linux publishes that file on arm64 and on Intel hybrids, and nothing like it exists on Windows or Apple, which read 0.
A kernel that rates every core alike rates nothing, so a tie also reads 0; Linux then splits the classes by their core PMUs, cpu_core and cpu_atom on Intel hybrids. compute_cache_bytes_per_core_in reports each logical core’s share of the deepest cache confined to a domain: one instance’s bytes over the cores sharing it.
It is enumerated exactly rather than measured — from the per-core cache hierarchy in sysfs on Linux, CPUID leaves elsewhere on x86, sysctl on Apple, and the cache relationships on Windows.
Domains of identical throughput can sit behind very differently sized caches, so neither value follows from the other.
Treat both as hints, present only where the hardware volunteers them, and never as a number a program requires.
The memory axis carries its own magnitudes, all answered by the harvested Fabric. memory_distance gives the relative cost of reaching a memory domain from a compute domain — 10 for local, higher for remote, on ACPI SLIT’s scale but from measured ratios — while memory_bandwidth and memory_latency give the observed saturated read bandwidth and dependent-load latency per edge.
Back on the Topology, volume_ram and volume_ram_in report installed RAM, total or per domain, while huge_pages_count_in and volume_huge_pages_in report the free huge pages a domain can still back an allocation with, as a count or in bytes, and huge_pages_count and volume_huge_pages total them.
Like the compute magnitudes, these are best-effort: fabric queries read 0 before a harvest or on edges no pinned worker could reach.
Finally, local_memory_of bridges the axes, naming the memory domain a given compute domain should allocate from.
Where no topology is harvested, every query degrades to a single compute domain and a single memory domain rather than failing, so these loops need no conditional compilation.
API
Types
memory_domain_id_tThe OS's NUMA node id - on Linux, an N for which /sys/devices/system/node/nodeN exists.
core_id_tOpaque logical-processor id; on Windows it packs a group and a bit.
enum core_id_t socket_id_tPhysical CPU socket, or -1 where the OS will not say.
enum socket_id_t compute_domain_index_tA position in machine_topology's array of compute domains, in [0, compute_domains_count).
enum compute_domain_index_t These are plain enums with a fixed underlying type, deliberately not enum class. The implicit widening to std::size_t survives, so comparisons and array subscripts work as on integers; what does not survive is passing one where the other is expected, because no conversion exists between two enumeration types. Minting one costs a static_cast, which is the point: it marks the spot where an untyped integer becomes a claim about which axis it indexes.
This is not hypothetical. RoundRobinVec handed a compute-domain index to an allocator expecting a memory-domain index, and nbody.cpp did the same to an array of per-node replicas. Both compiled, both ran on every machine where the two counts happened to match, and both broke on the first chip with three compute domains over one memory domain.
The OS IDs above - cores, memory domains and sockets - are plain enums for the same reason, and so is compute_level_t, so a QoS rank never passes for a domain position.
See also
memory_domain_index_t, which indexes a different array and must not be confused with this.
Notes
The implicit widening also means a raw array[compute_domain] still compiles. The enums stop the argument-passing mistake, not the subscript one. Rust's newtypes stop both.
memory_domain_index_tA position in machine_topology's array of memory domains, in [0, memory_domains_count).
enum memory_domain_index_t compute_level_tA rung of the dense QoS ladder, in [0, compute_levels_count), least performant first.
enum compute_level_t thread_in_domainOne thread, situated in one compute domain - the "where I am" every callback receives.
template <typename index_type_ = std::size_t>
struct thread_in_domain Every dispatch hands its callback two things: the work, and this. A callback that only needs the thread can take the implicit conversion to a bare index; one placing memory reads compute_domain to find the node it runs on.
Fields
thread | thread_index_t | The thread or core index, in [0, threads_count). |
|---|---|---|
compute_domain | compute_domain_index_t | The compute domain the thread is pinned to, in [0, compute_domains_count). |
ram_page_setting_tOne page size the kernel offers, and how many pages of it exist.
struct ram_page_setting_t A machine reports several: the base page every allocation uses by default, and whichever huge page sizes the hardware and kernel agree on. available_pages counts what was reserved, and free_pages what nobody took yet, letting an allocator tell "unsupported" from "exhausted".
Fields
bytes_per_page | std::size_t | Huge page size in bytes, e.g. |
|---|---|---|
available_pages | std::size_t | Number of pages available for this size, 0 if not available. |
free_pages | std::size_t | Number of pages available and unused, 0 if not available. |
See also
memory_domainDescribes a NUMA node, containing its ID, memory size, and core IDs.
template <std::size_t max_page_sizes_ = 4>
struct memory_domain Fields
max_page_sizes_k | std::size_t | |
|---|---|---|
memory_domain_id | memory_domain_id_t | The OS's id for this memory domain: a NUMA node number, in [0, numa_max_node()]. |
socket_id | socket_id_t | Physical CPU socket ID. |
volume_ram | std::size_t | RAM volume in bytes. |
core_ids | std::span<core_id_t const> | This node's logical cores, a slice of the topology's core array. |
page_sizes | ram_page_settings<max_page_sizes_k> | Huge page sizes available on this memory domain. |
See also
Views different slices of the machine_topology structure.
compute_domain_tOne bindable cluster of cores sharing a QoS class and locality - the compute axis.
struct compute_domain_t A compute domain is a contiguous run of same-capacity cores within a single NUMA node. It is what a pool binds to and the index a worker callback receives. Several compute domains may map to one memory domain - performance and efficiency cores sharing a memory controller - which is why compute and memory are separate axes rather than a single "colocation" cell.
Fields
memory_domain_id | memory_domain_id_t | The OS's id for the memory domain these cores live on. |
|---|---|---|
memory_domain_index | memory_domain_index_t | Our dense index for that same memory domain, into machine topology's array. |
compute_level | compute_level_t | QoS ordinal, sorted least-to-most performant. |
capacity | std::size_t | Relative throughput of one core here; 0 when the platform exposes no rating. |
cache_bytes_per_core | std::size_t | Bytes of the deepest cache confined to this domain, divided among the logical cores sharing one instance of it; 0 when unknown. |
core_ids | std::span<core_id_t const> | This domain's logical cores, a slice of the topology's core array. |
See also
memory_domain is the memory axis; a machine_topology exposes both plus their affinity.
core_order_tOne core's place in its node's compute split, read by the harvest's sorts.
struct core_order_t Fields
capacity | std::uint32_t | Scheduler capacity; 0 where the kernel publishes none or rates every core alike. |
|---|---|---|
class_max_frequency_khz | std::uint32_t | Highest cpuinfo_max_freq among its core PMU's cores, set only when capacities tie. |
cluster_core_id | core_id_t | Lowest core of the cluster cache it sits under; -1 when its whole class is one cluster. |
sibling_rank | std::uint32_t | 0 for a physical core's first hardware thread, 1 for its sibling. |
cache_instance_tOne instance of a cache: its bytes and the logical cores sharing it.
struct cache_instance_t Fields
bytes | std::size_t | 0 when no platform source names the cache. |
|---|---|---|
sharing_cores | std::uint32_t | Logical cores listed in its shared_cpu_list, or CPUID's sharing width. |
first_core_id | core_id_t | Lowest core sharing it; -1 where only CPUID answered. |
measured_edge_tOne recorded observation of a memory-fabric edge: what initiator's cores see when reaching memory resident on target.
struct measured_edge_t The chase and the stream are separate experiments, so each record carries its own experiment's metric and 0 for the other; measured_fabric folds the log into per-metric envelopes.
Fields
initiator | compute_domain_index_t | The compute domain whose cores issued the loads - the pool's pinning unit, never a memory domain: sibling domains on one controller still measure apart. |
|---|---|---|
target | memory_domain_index_t | The memory domain whose DRAM answered the loads. |
nanoseconds | std::size_t | Dependent-load latency of a lone core, in nanoseconds; 0 if unobserved. |
megabytes_per_second | std::size_t | Saturated read bandwidth of all the initiator's cores at once, in MB/s; 0 if unobserved. |
chase_slot_tOne hop of the latency walk, padded to a cache line so consecutive slots never share one.
struct chase_slot_t Fields
next_slot_index | std::uint32_t | Index of the slot the walk visits next; no initializer, as resize_uninitialized demands trivial construction and thread_chase_list_ writes every slot anyway. |
|---|
logging_colors_tDetects if the output stream supports ANSI color codes.
struct logging_colors_t Notes
Every accessor below returns its escape sequence when colors are on, and an empty string when they are off, so call sites interpolate them unconditionally.
log_memory_volume_tFormats memory volume in bytes with appropriate units and precision, like "1.5 GB".
struct log_memory_volume_t log_core_range_tFormats a set of CPU core IDs in a compact and readable way, like "0-3,5,7,8,10-12".
struct log_core_range_t log_numa_topology_tNUMA topology logger with compact tree design and color support.
struct log_numa_topology_t Classcore_maskA dense bitset over core_id_t: the cores a thread may run on, or is confined to.4
template <typename allocator_type_ = std::allocator<core_mask_word_t>>
class core_mask A machine is not the same thing as the slice of it we were handed. taskset, a cgroup cpuset, and a batch scheduler all narrow this set, and hardware_concurrency sees none of them. Sizing a pool from the machine and pinning to cores outside the set either escapes the restriction or, where the kernel enforces it, crowds every spinning worker onto the few cores left inside it.
Every platform hands out a dense core id, so one bitset covers them all: Linux and FreeBSD number logical processors from zero, and Windows packs (group << 6) | bit into one integer.
glibc's __sched_cpualloc rounds the count up and tail-calls malloc. A dynamic_array sized from possible_cores() costs one cold-path allocation and, unlike a fixed cpu_set_t, never stops at glibc's 1024-core CPU_SETSIZE.
Notes
Not CPU_ALLOC, a malloc behind a name, which would both bypass this allocator and contradict what the library promises.
core_mask
core_mask() noexcept=default; core_mask(allocator_type_ const & allocator) noexcept; resize_forSizes the mask for cores, leaving it unusable rather than half-sized when the allocation fails.
status_t resize_for(std::size_t const cores) noexcept; resizeSizes the mask to hold every id this machine can produce, per id_space.
status_t resize() noexcept; id_spaceUpper bound on core_id_t values this machine can produce, the width a mask covers.
std::size_t id_space() noexcept; Notes
Not possible_cores(); Windows ids are (group << 6) | bit, so a two-group machine with 80 processors still emits ids up to 103, and sizing by core count would drop them.
Classmeasured_fabricThe measured memory fabric - what this process observed, as opposed to the structure the OS declared in machine_topology.12
template <typename allocator_type_ = std::allocator<char>>
class measured_fabric Two query families: edge queries (initiator,target) describe one interconnect link; medium queries (target) describe the memory pool itself, independent of any initiator.
Completes the harvest pipeline: a machine_topology is harvested first and stays immutable, a pool spawns on it, and the fabric then harvests through that pool's workers, snapshotting what it needs so the topology may be freed after. harvest is the only mutator and replaces the whole snapshot; before it, every query answers 0 and memory_levels_count answers 1.
measured_fabric
constexpr measured_fabric() noexcept=default; measured_fabric(measured_fabric && o) noexcept; measured_fabric(measured_fabric const &)=delete; operator=
measured_fabric & operator=(measured_fabric && other) noexcept; measured_fabric & operator=(measured_fabric const &)=delete; ~measured_fabric
~measured_fabric() noexcept; *_domains_count× 2Snapshot of the topology's compute domain count at harvest time; 0 before any harvest.
std::size_t compute_domains_count() const noexcept; | Symbol |
|---|
compute_domains_count |
memory_domains_count |
memory_latencyBest dependent-load latency per edge, in nanoseconds; 0 if unwalked or out of range.
std::size_t memory_latency(
compute_domain_index_t const initiator,
memory_domain_index_t const target) const noexcept; Notes
The minimum across recordings, since interference only ever adds nanoseconds.
memory_bandwidthBest saturated read bandwidth per edge, in MB/s; 0 if unwalked or out of range.
std::size_t memory_bandwidth(
compute_domain_index_t const initiator,
memory_domain_index_t const target) const noexcept; Notes
The maximum across recordings, since interference only ever subtracts megabytes.
memory_distanceRelative access distance on an edge, 10 = local per SLIT; 0 if out of range.
std::size_t memory_distance(
compute_domain_index_t const initiator,
memory_domain_index_t const target) const noexcept; The measured latency ratio to the initiator's local domain, times 10, rounded half-up and clamped to at least 10, so the local domain always carries the row's minimum. Unwalked edges fall back to 10-local / 20-remote; unclamped nanoseconds live in memory_latency.
memory_level_inThe pool's derived speed class, 0 = fastest; keyed by the best bandwidth any initiator sustains to it, ties split by the best latency.
std::size_t memory_level_in(
memory_domain_index_t const memory_domain_index) const noexcept; 0 if out of range.
Notes
Tier boundaries are measurement-derived and can shift between harvests.
memory_levels_countNumber of distinct derived memory tiers, at least 1.
std::size_t memory_levels_count() const noexcept; edges_countNumber of recorded observations; each carries one experiment's metrics for one edge.
std::size_t edges_count() const noexcept; edge_atThe observation at index, in [0, edges_count()).
measured_edge_t const & edge_at(std::size_t const index) const noexcept; harvestHarvests every reachable edge and the tiers derived from them through pool's workers, replacing any previous snapshot.
template <typename pool_type_>
status_t harvest(
machine_topology_t const & topology,
pool_type_ & pool) noexcept; The topology is only read.
Targets are the memory domains some worker can first-touch; cpuless domains, like CXL expanders, stay unwalked, since portable first-touch cannot place pages there.
Notes
Not thread-safe: dispatches on the pool and rebuilds this fabric, so call it between task batches and do not query concurrently. Expect seconds of runtime on large fabrics.
Statuses
config_mismatch_kThe pool cannot place pages on every domain it would walk, as a terminated pool or an unpinned one on a machine of several domains cannot.
Classram_page_settingsDescribes the memory pages sizes configured & supported by the OS & CPU.3
template <std::size_t max_page_sizes_ = 4>
class ram_page_settings This class avoids HugeTLBfs in favor of a direct access to the /sys filesystem. Aside from fetching the stats, it also allows us to change settings if admin privileges are granted to running process.
Huge Pages and Transparent Huge Pages
Virtual Address Space, or VAS, is divided into pages, typically 4 KB in size. Converting a virtual address to a physical address requires a page table lookup. Think of it as a hash table... and as everyone knows, hash table lookups and updates aren't free, so most chips have a "Translation Lookaside Buffer" TLB cache as part of the "Memory Management Unit" MMU to speed up the process.
To keep it fast, in Big Data applications, one would like to use larger pages, to reduce the number of distinct entries in the TLB cache. Going from 4 KB to 2 MB or 1 GB "Huge Pages" HPs, reduces the table size by 512 or 262K times, respectively.
To benefit from those, some applications rely on "Transparent Huge Pages" THP, which are automatically allocated by the kernel. Such implicit behaviour isn't great for performance-oriented applications, so this inventory exposes largest_free, which the linux_numa_allocator consults before falling back to the base page size.
See also
largest_freeFinds the largest Huge Pages size available for the given NUMA node.
ram_page_setting_t largest_free() const noexcept; harvestFetches all available huge page sizes for the given NUMA node.
status_t harvest(memory_domain_id_t memory_domain_id) noexcept; Notes
Kernel support doesn't mean that pages of that size have a valid mount point.
changeAttempts to reserve huge pages of a specific size on the current NUMA node.
status_t change(
memory_domain_id_t memory_domain_id,
std::size_t page_size_bytes,
std::size_t num_pages) noexcept; Parameters
memory_domain_id | in | The NUMA node to reserve the pages on. |
|---|---|---|
page_size_bytes | in | The size of huge pages to reserve - must match an available size |
num_pages | in | Number of pages to reserve |
Returns
true if reservation was successful, false otherwise
Notes
Requires root privileges or appropriate capabilities
Structmachine_topologyNUMA topology descriptor: describing memory pools and core counts next to them.12
template <std::size_t max_page_sizes_ = 4,
typename allocator_type_ = std::allocator<char>>
struct machine_topology Uses dynamic memory to store the NUMA nodes and their cores. Assuming we may soon have Intel "Sierra Forest"-like CPUs with 288 cores with up to 8 sockets per node, this structure can easily grow to 10 KB.
machine_topology
constexpr machine_topology() noexcept=default; machine_topology(machine_topology && o) noexcept; machine_topology(machine_topology const &)=delete; operator=
machine_topology & operator=(machine_topology && other) noexcept; machine_topology & operator=(machine_topology const &)=delete; ~machine_topology
~machine_topology() noexcept; assignCopy-assigns the topology from other.
status_t assign(machine_topology const & other) noexcept; Instead of a copy-constructor we expose an explicit operation that can fail - returning false if any intermediate allocation fails. On success the current instance owns a deep copy, and on failure the current instance is unchanged.
Parameters
other | in | The topology to deep-copy from, left unchanged. |
|---|
memory_domains_countNumber of memory domains, one per NUMA node; see also compute_domains_count.
std::size_t memory_domains_count() const noexcept; memory_domain_atThe memory domain at memory_domain_index, in [0, memory_domains_count()).
memory_domain_t const & memory_domain_at(
memory_domain_index_t const memory_domain_index) const noexcept; compute_domains_countNumber of compute domains - one per same-QoS core run within a node.
std::size_t compute_domains_count() const noexcept; compute_levels_countNumber of distinct QoS classes across all compute domains, at least 1.
std::size_t compute_levels_count() const noexcept; compute_domain_atThe compute domain at compute_domain_index, in [0, compute_domains_count()).
compute_domain_t const & compute_domain_at(
compute_domain_index_t const compute_domain_index) const noexcept; local_memory_ofThe memory domain nearest a compute domain - its NUMA node; 0 if out of range.
memory_domain_index_t local_memory_of(
compute_domain_index_t const compute_domain_index) const noexcept; harvest_portableFills a single memory domain and compute domain covering every allowed core.
status_t harvest_portable() noexcept; The uniform view used when no richer topology source exists - a build without FORKUNION_WITH_TOPOLOGY, or a machine the kernel reports no NUMA for. Every query then returns a sensible whole-machine answer and a distributed_pool degenerates to one domain, so fu_topology_t is usable anywhere.
Returns
false only if the core count is zero or an allocation fails.
harvestHarvests CPU-memory topology - Linux NUMA nodes, or Apple Silicon performance levels.
status_t harvest() noexcept; Falls back to try_harvest_portable whenever no richer source is available, so a spawned pool always sees at least one compute and one memory domain.
Returns
true if the harvest was successful and the topology is ready to use, or false if the platform lacks topology support or the harvest failed.
Functions
current_thread_idThe kernel's own identifier for the calling thread, or 0 where there is none.
std::uint64_t current_thread_id() noexcept; Linux calls it a pid_t and hands it out through gettid. Darwin has no gettid at all, and spells the same idea pthread_threadid_np, returning 64 bits. Both are the number a scheduler or a profiler will show you; neither is a pthread_t.
See also
pinned_thread_t::id, which caches it so other threads can read it.
set_current_thread_nameNames the calling thread, which is the only thread every platform lets us name.
void set_current_thread_name(char const *thread_name) noexcept; Linux's pthread_setname_np takes a thread and a name, so a spawner can name its workers. Apple's takes only a name and always renames the caller. Rather than branch on that at every call, the worker names itself once it is running - the one shape both kernels agree on.
possible_coresUpper bound on core IDs this machine may ever report, for sizing masks and names.
std::size_t possible_cores() noexcept; Not the same as hardware_concurrency() on Linux, where cores can be hot-plugged and the kernel reserves IDs for cores that are offline right now. Elsewhere the distinction does not exist.
capture_thread_coresReads the cores the calling thread may run on into cores.
status_t capture_thread_cores(core_mask_t & cores) noexcept; Returns
false where the platform exposes no such mask, which is not an error.
allowed_cores_countNumber of cores the calling thread may run on, or possible_cores() where unknowable.
std::size_t allowed_cores_count() noexcept; Notes
Prefer this to std::thread::hardware_concurrency when sizing a pool: the latter counts the machine's cores, not the ones this process was given.
apply_thread_coresConfines thread to the cores held by cores.
status_t apply_thread_cores(
native_thread_t thread,
core_mask_t const & cores) noexcept; The one place placement happens.
Linux hands out a cpu_set_t and honours it. FreeBSD spells the same idea cpuset_t. Windows addresses a core by processor group and bit, packed into each core_id_t; a thread lives in exactly one group, so a mask spanning two is a caller error. Apple Silicon answers KERN_NOT_SUPPORTED to thread_policy_set - measured, not assumed - and offers only a Quality-of-Service class, chosen at creation. So a pool there partitions the work by domain and lets the scheduler place the threads.
Returns
false when the platform exposes no thread placement, which is not an error.
See also
try_pin_thread_to_cores, the adaptor that builds a mask from a core list.
pin_thread_to_coresConfines thread to the count cores listed in cores.
status_t pin_thread_to_cores(
native_thread_t,
std::span<core_id_t const>) noexcept; Returns
false when the platform exposes no thread placement, which is not an error.
Notes
A thin adaptor: it builds a core_mask and defers to try_apply_thread_cores, which is where the per-platform placement lives. It owns no platform logic of its own.
restore_thread_coresPuts the calling thread back on the cores saved recorded before it was pinned.
status_t restore_thread_cores(core_mask_t const & saved) noexcept; A pool that narrows the caller owes it the mask it had, not the mask of the whole machine. The two differ under taskset, a cgroup cpuset, or any batch scheduler, and widening to the machine would hand the caller cores this process was never granted.
Note that no NUMA run-node policy is reset here: the pool never sets one. Memory placement goes through mbind on the allocation, not through the calling thread's policy, and numa_run_on_node would rewrite the very CPU mask we just restored.
Notes
A no-op where nothing was ever narrowed, or where saved was never captured.
ram_page_sizeFetches the RAM page size in bytes.
std::size_t ram_page_size() noexcept; Returns
The size of a memory page in bytes, typically 4096 on most systems.
Notes
On Linux, this is the system page size, which may differ from Huge Pages sizes.
volume_ramFetches the total RAM amount available on the system in bytes.
std::size_t volume_ram() noexcept; Returns
Total system RAM in bytes, or 0 if detection fails.
Notes
This function provides cross-platform detection of total physical memory.
for_each_id_list_rangeHands every [low, high] range in a Linux id list - "0", "0-3", "0,2-4" - to visit.
template <typename visitor_type_>
void for_each_id_list_range(
char const *line,
visitor_type_ && visit) noexcept; Notes
One parser for every list the kernel publishes: shared_cpu_list, a domain's cpulist, and node/online. Ranges arrive inclusive on both ends, as written.
cpu_list_withinWhether every core in a Linux cpulist line - "0", "0-3", "0,2-4" - belongs to cores.
bool cpu_list_within(
char const *line,
std::span<core_id_t const> cores) noexcept; Notes
Pass only complete lines: a truncated tail could name cores the verdict never saw.
deepest_cache_of_coreThe deepest data or unified cache serving core_id, confined to domain_cores.
cache_instance_t deepest_cache_of_core(
core_id_t core_id,
std::span<core_id_t const> domain_cores) noexcept; Two exact sources, no measurement: Linux's per-core cacheinfo sysfs, counting a level only if its shared_cpu_list stays within the domain - a socket-wide L3 is not one QoS class's to claim; elsewhere x86 CPUID leaf 0x4 / 0x8000001D, with the sharing width standing in for that containment. Arm has no userspace cache-geometry registers - CCSIDR_EL1 is EL1-only - so sysfs is the only Arm source and other Arm hosts honestly report 0.
Returns
The cache instance, with zero bytes when no platform source can name it.
cache_line_bytesBytes in one cache line - what a single access transfers.
std::size_t cache_line_bytes() noexcept; Exact sources only, no measurement: Linux's coherency_line_size - the only source on Arm, where CCSIDR_EL1 is EL1-only - Apple's hw.cachelinesize, Windows' CACHE_RELATIONSHIP, and x86 CPUID leaf 0x4, whose line field deepest_cache_of_core above already decodes for its size arithmetic. Every source is gated to a power of two in [16, 1024], so a bogus reading falls through to the caller's compiled default rather than sizing an arena.
Returns
The line size, or 0 when no platform source names it.
destructive_interference_bytesBytes two cores can invalidate for each other - what padding must span.
std::size_t destructive_interference_bytes() noexcept; Not the same question as cache_line_bytes: x86's adjacent-line prefetcher pulls lines in pairs, so two 64-byte lines behave as one 128-byte unit of coherence traffic and padding must cover both. Everywhere else the line is the unit. This is the quantity default_alignment_k guesses from the target when no probe can run.
Returns
The destructive-interference width, or 0 where the line size is unknown.
Constants
unreachable_position_kMarks a fabric position the pool has no worker on - cpuless, or beyond a partial spawn.
std::size_t unreachable_position_k; TypesMember types8
measured_fabric::allocator_t | allocator_type_ |
|---|---|
measured_fabric::edges_allocator_t | typename std::allocator_traits<allocator_t>::template rebind_alloc<measured_edge_t> |
measured_fabric::indices_allocator_t | typename std::allocator_traits<allocator_t>::template rebind_alloc<std::size_t> |
machine_topology::allocator_t | allocator_type_ |
machine_topology::cores_allocator_t | typename std::allocator_traits<allocator_t>::template rebind_alloc<core_id_t> |
machine_topology::memory_domains_allocator_t | typename std::allocator_traits<allocator_t>::template rebind_alloc<memory_domain_t> |
machine_topology::domains_allocator_t | typename std::allocator_traits<allocator_t>::template rebind_alloc<compute_domain_t> |
machine_topology::core_orders_allocator_t | typename std::allocator_traits<allocator_t>::template rebind_alloc<core_order_t> |