C++ · Topology

Topology

#include <forkunion/topology.hpp>#include <forkunion/distributed.hpp>#include <forkunion/types.hpp>#include <forkunion/logging.hpp> 4.0.0-rc.1 · d927445f

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 axisMemory axis
Count domainscompute_domains_countmemory_domains_count
Count levelscompute_levels_countmemory_levels_count
Level of a domaincompute_level_inmemory_level_in
Faster meanshigherlower

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.
types.hpp:647 ↗
enum memory_domain_id_t
core_id_tOpaque logical-processor id; on Windows it packs a group and a bit.
types.hpp:650 ↗
enum core_id_t
socket_id_tPhysical CPU socket, or -1 where the OS will not say.
types.hpp:653 ↗
enum socket_id_t
compute_domain_index_tA position in machine_topology's array of compute domains, in [0, compute_domains_count).
types.hpp:704 ↗
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).
types.hpp:707 ↗
enum memory_domain_index_t
compute_level_tA rung of the dense QoS ladder, in [0, compute_levels_count), least performant first.
types.hpp:710 ↗
enum compute_level_t
thread_in_domainOne thread, situated in one compute domain - the "where I am" every callback receives.
types.hpp:1326 ↗
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

threadthread_index_tThe thread or core index, in [0, threads_count).
compute_domaincompute_domain_index_tThe 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.
topology.hpp:459 ↗
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_pagestd::size_tHuge page size in bytes, e.g.
available_pagesstd::size_tNumber of pages available for this size, 0 if not available.
free_pagesstd::size_tNumber of pages available and unused, 0 if not available.

See also

memory_domainDescribes a NUMA node, containing its ID, memory size, and core IDs.
topology.hpp:786 ↗
template <std::size_t max_page_sizes_ = 4>
struct memory_domain

Fields

max_page_sizes_kstd::size_t
memory_domain_idmemory_domain_id_tThe OS's id for this memory domain: a NUMA node number, in [0, numa_max_node()].
socket_idsocket_id_tPhysical CPU socket ID.
volume_ramstd::size_tRAM volume in bytes.
core_idsstd::span<core_id_t const>This node's logical cores, a slice of the topology's core array.
page_sizesram_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.
topology.hpp:832 ↗
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_idmemory_domain_id_tThe OS's id for the memory domain these cores live on.
memory_domain_indexmemory_domain_index_tOur dense index for that same memory domain, into machine topology's array.
compute_levelcompute_level_tQoS ordinal, sorted least-to-most performant.
capacitystd::size_tRelative throughput of one core here; 0 when the platform exposes no rating.
cache_bytes_per_corestd::size_tBytes of the deepest cache confined to this domain, divided among the logical cores sharing one instance of it; 0 when unknown.
core_idsstd::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.
topology.hpp:919 ↗
struct core_order_t

Fields

capacitystd::uint32_tScheduler capacity; 0 where the kernel publishes none or rates every core alike.
class_max_frequency_khzstd::uint32_tHighest cpuinfo_max_freq among its core PMU's cores, set only when capacities tie.
cluster_core_idcore_id_tLowest core of the cluster cache it sits under; -1 when its whole class is one cluster.
sibling_rankstd::uint32_t0 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.
topology.hpp:939 ↗
struct cache_instance_t

Fields

bytesstd::size_t0 when no platform source names the cache.
sharing_coresstd::uint32_tLogical cores listed in its shared_cpu_list, or CPUID's sharing width.
first_core_idcore_id_tLowest 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.
distributed.hpp:1874 ↗
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

initiatorcompute_domain_index_tThe compute domain whose cores issued the loads - the pool's pinning unit, never a memory domain: sibling domains on one controller still measure apart.
targetmemory_domain_index_tThe memory domain whose DRAM answered the loads.
nanosecondsstd::size_tDependent-load latency of a lone core, in nanoseconds; 0 if unobserved.
megabytes_per_secondstd::size_tSaturated 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.
distributed.hpp:1891 ↗
struct chase_slot_t

Fields

next_slot_indexstd::uint32_tIndex 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.
logging.hpp:19 ↗
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".
logging.hpp:79 ↗
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".
logging.hpp:125 ↗
struct log_core_range_t
log_numa_topology_tNUMA topology logger with compact tree design and color support.
logging.hpp:186 ↗
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
topology.hpp:161 ↗
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
topology.hpp:168 ↗
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.
topology.hpp:187 ↗
status_t resize_for(std::size_t const cores) noexcept;
resizeSizes the mask to hold every id this machine can produce, per id_space.
topology.hpp:190 ↗
status_t resize() noexcept;
id_spaceUpper bound on core_id_t values this machine can produce, the width a mask covers.
topology.hpp:176 ↗
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
distributed.hpp:2155 ↗
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
distributed.hpp:2185 ↗
constexpr measured_fabric() noexcept=default;
measured_fabric(measured_fabric && o) noexcept;
measured_fabric(measured_fabric const &)=delete;
operator=
distributed.hpp:2193 ↗
measured_fabric & operator=(measured_fabric && other) noexcept;
measured_fabric & operator=(measured_fabric const &)=delete;
~measured_fabric
distributed.hpp:2209 ↗
~measured_fabric() noexcept;
*_domains_count× 2Snapshot of the topology's compute domain count at harvest time; 0 before any harvest.
distributed.hpp:2220 ↗
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.
distributed.hpp:2229 ↗
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.
distributed.hpp:2245 ↗
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.
distributed.hpp:2264 ↗
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.
distributed.hpp:2279 ↗
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.
distributed.hpp:2285 ↗
std::size_t memory_levels_count() const noexcept;
edges_countNumber of recorded observations; each carries one experiment's metrics for one edge.
distributed.hpp:2288 ↗
std::size_t edges_count() const noexcept;
edge_atThe observation at index, in [0, edges_count()).
distributed.hpp:2291 ↗
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.
distributed.hpp:2310 ↗
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_k The 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
topology.hpp:570 ↗
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.
topology.hpp:581 ↗
ram_page_setting_t largest_free() const noexcept;
harvestFetches all available huge page sizes for the given NUMA node.
topology.hpp:602 ↗
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.
topology.hpp:736 ↗
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_idinThe NUMA node to reserve the pages on.
page_size_bytesinThe size of huge pages to reserve - must match an available size
num_pagesinNumber 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
topology.hpp:1485 ↗
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
topology.hpp:1523 ↗
constexpr machine_topology() noexcept=default;
machine_topology(machine_topology && o) noexcept;
machine_topology(machine_topology const &)=delete;
operator=
topology.hpp:1535 ↗
machine_topology & operator=(machine_topology && other) noexcept;
machine_topology & operator=(machine_topology const &)=delete;
~machine_topology
topology.hpp:1552 ↗
~machine_topology() noexcept;
assignCopy-assigns the topology from other.
topology.hpp:1571 ↗
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

otherinThe topology to deep-copy from, left unchanged.
memory_domains_countNumber of memory domains, one per NUMA node; see also compute_domains_count.
topology.hpp:1618 ↗
std::size_t memory_domains_count() const noexcept;
memory_domain_atThe memory domain at memory_domain_index, in [0, memory_domains_count()).
topology.hpp:1622 ↗
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.
topology.hpp:1628 ↗
std::size_t compute_domains_count() const noexcept;
compute_levels_countNumber of distinct QoS classes across all compute domains, at least 1.
topology.hpp:1631 ↗
std::size_t compute_levels_count() const noexcept;
compute_domain_atThe compute domain at compute_domain_index, in [0, compute_domains_count()).
topology.hpp:1634 ↗
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.
topology.hpp:1640 ↗
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.
topology.hpp:1658 ↗
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.
topology.hpp:1843 ↗
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.
topology.hpp:28 ↗
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.
topology.hpp:54 ↗
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.
topology.hpp:89 ↗
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.
topology.hpp:231 ↗
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.
topology.hpp:277 ↗
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.
topology.hpp:314 ↗
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.
topology.hpp:378 ↗
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.
topology.hpp:395 ↗
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.
topology.hpp:479 ↗
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.
topology.hpp:496 ↗
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.
topology.hpp:889 ↗
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.
topology.hpp:906 ↗
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.
topology.hpp:964 ↗
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.
topology.hpp:1060 ↗
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.
topology.hpp:1124 ↗
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.
distributed.hpp:1992 ↗
std::size_t unreachable_position_k;
TypesMember types8
measured_fabric::allocator_tallocator_type_
measured_fabric::edges_allocator_ttypename std::allocator_traits<allocator_t>::template rebind_alloc<measured_edge_t>
measured_fabric::indices_allocator_ttypename std::allocator_traits<allocator_t>::template rebind_alloc<std::size_t>
machine_topology::allocator_tallocator_type_
machine_topology::cores_allocator_ttypename std::allocator_traits<allocator_t>::template rebind_alloc<core_id_t>
machine_topology::memory_domains_allocator_ttypename std::allocator_traits<allocator_t>::template rebind_alloc<memory_domain_t>
machine_topology::domains_allocator_ttypename std::allocator_traits<allocator_t>::template rebind_alloc<compute_domain_t>
machine_topology::core_orders_allocator_ttypename std::allocator_traits<allocator_t>::template rebind_alloc<core_order_t>