region_cached/lib.rs
1#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3
4//! On many-processor systems with multiple memory regions, there is an extra cost associated with
5//! accessing data in physical memory modules that are in a different memory region than the current
6//! processor:
7//!
8//! * Cross-memory-region loads have higher latency (e.g. 100 ns local versus 200 ns remote).
9//! * Cross-memory-region loads have lower throughput (e.g. 200 GBps local versus 100 GBps remote).
10//!
11//! This crate provides the capability to cache frequently accessed shared data sets
12//! in the local memory region, speeding up reads when the data is not already in the
13//! local processor caches.
14#![doc = mermaid!("../doc/region_cached.mermaid")]
15//!
16//! Think of this as an extra level of caching between L3 processor caches and main memory.
17//!
18//! This is part of the [Folo project](https://github.com/folo-rs/folo) that provides mechanisms for
19//! high-performance hardware-aware programming in Rust.
20//!
21//! # Applicability
22//!
23//! A positive performance impact may be seen if all of the following conditions are true:
24//!
25//! 1. The system has multiple memory regions.
26//! 2. A shared data set is accessed from processors in different memory regions.
27//! 3. The data set is large enough to make it unlikely to be resident in local processor caches.
28//! 4. There is sufficient memory capacity to clone the data set into every memory region.
29//!
30//! As with all performance and efficiency questions, you should use a profiler to measure real impact.
31//!
32//! # Usage
33//!
34//! There are two ways to create region-cached values:
35//!
36//! 1. Define a static variable in a [`region_cached!`][2] block.
37//! 2. Use the [`RegionCached`][5] type inside a [`linked::InstancePerThread<T>`][4]
38//! or [`linked::InstancePerThreadSync<T>`][7] wrapper.
39//!
40//! The difference is only a question of convenience - static variables are easier to use but come
41//! with language-driven limitations, such as needing to know in advance how many you need and
42//! defining them in the code.
43//!
44//! In contrast, `linked::InstancePerThread<RegionCached<T>>` is more flexible and you can create
45//! any number of instances at runtime, at a cost of having to manually deliver instances to
46//! the right place in the code.
47//!
48//! ## Usage via static variables
49//!
50//! This crate provides the [`region_cached!`][2] macro that enhances static variables with
51//! region-local caching and provides interior mutability via weakly consistent writes.
52//!
53//! ```
54//! // RegionCachedExt provides required extension methods on region-cached
55//! // static variables, such as `with_cached()` and `set_global()`.
56//! use region_cached::{region_cached, RegionCachedExt};
57//!
58//! region_cached!(static FAVORITE_COLOR: String = "blue".to_string());
59//!
60//! FAVORITE_COLOR.with_cached(|color| {
61//! println!("My favorite color is {color}");
62//! });
63//!
64//! FAVORITE_COLOR.set_global("red".to_string());
65//! ```
66//!
67//! See `examples/region_cached_log_filtering.rs` for a more complete example of using this macro.
68//!
69//! ## Usage via `InstancePerThreadSync<RegionCached<T>>`
70//!
71//! There exist situations where a static variable is not suitable. For example, the number of
72//! different region-cached objects may be determined at runtime (e.g. a separate value
73//! for each log source loaded from configuration).
74//!
75//! In this case, you can directly use the [`RegionCached`][5] type which underpins the mechanisms
76//! exposed by the macro. This type is implemented using the [linked object pattern][3] and
77//! can be manually used via the [`InstancePerThread<T>`][4] or
78//! [`InstancePerThreadSync<T>`][7] wrapper type, as `InstancePerThreadSync<RegionCached<T>>`.
79//!
80//! ```
81//! use linked::InstancePerThreadSync;
82//! use region_cached::RegionCached;
83//!
84//! let favorite_color_regional = InstancePerThreadSync::new(RegionCached::new("blue".to_string()));
85//!
86//! // This localizes the object to the current thread. Reuse this value when possible.
87//! let favorite_color = favorite_color_regional.acquire();
88//!
89//! favorite_color.with_cached(|color| {
90//! println!("My favorite color is {color}");
91//! });
92//!
93//! favorite_color.set_global("red".to_string());
94//! ```
95//!
96//! See `examples/region_cached_log_filtering_no_statics.rs` for a more complete example of
97//! dynamically stored region-cached values that do not require static variables.
98//!
99//! See the documentation of the [`linked`][linked] crate for more details on the mechanisms
100//! offered by the linked object pattern. Additional capabilities exist beyond those described here.
101//!
102//! # Consistency guarantees
103//! [consistency-guarantees]: [#consistency-guarantees]
104//!
105//! Writes are weakly consistent, with an undefined order of resolving from different threads.
106//! Writes from the same thread become visible sequentially on all threads.
107//!
108//! Writes are immediately visible from the originating thread, with the caveats that:
109//! 1. Writes from other threads may be applied at any time, such as between
110//! a local write and an immediately following read.
111//! 2. A thread, if not pinned, may migrate to a new memory region between the write and read
112//! operations, which invalidates any causal link between the two operations.
113//!
114//! In general, you can only have firm expectations about the sequencing of data produced by read
115//! operations if the writes are always performed from a single thread and reads on region-pinned
116//! threads.
117//!
118//! # Operating system compatibility
119//!
120//! This crate relies on the collaboration between the Rust global allocator and the operating
121//! system to map virtual memory pages to the correct memory region. The default configuration
122//! in operating systems tends to encourage region-local mapping but this is not guaranteed.
123//!
124//! Some evidence suggests that on Windows, region-local mapping is only enabled when the threads
125//! are pinned to specific processors in specific memory regions. A similar requirement is not known
126//! for Linux (at least Ubuntu 24) but this may differ based on the specific OS and configuration.
127//! Perform your own measurements to identify the behavior of your system and adjust the application
128//! structure accordingly.
129//!
130//! Example of using this crate with processor-pinned threads (`examples/region_cached_1gb.rs`):
131//!
132//! ```
133//! # use std::{hint::black_box, thread, time::Duration};
134//! # use many_cpus::SystemHardware;
135//! # use region_cached::{RegionCachedExt, region_cached};
136//! region_cached! {
137//! // We allocate a 1 GB object in every memory region.
138//! // There will also be one "global" copy in addition to the region-local copies.
139//! // With 4 memory regions, you should see a total of 5 GB allocated.
140//! static DATA: Vec<u8> = vec![50; 1024 * 1024 * 1024];
141//! }
142//!
143//! fn main() {
144//! let processor_set = SystemHardware::current().processors();
145//!
146//! processor_set
147//! .spawn_threads(|_| DATA.with_cached(|data| _ = black_box(data.len())))
148//! .into_iter()
149//! .for_each(|x| x.join().unwrap());
150//!
151//! println!(
152//! "All {} threads have accessed the region-cached data. Terminating in 60 seconds.",
153//! processor_set.len()
154//! );
155//!
156//! # #[cfg(doc)] // Only for show, do not run when testing.
157//! thread::sleep(Duration::from_mins(1));
158//! }
159//! ```
160//!
161//! # Cross-region visibility
162//!
163//! This type makes the value visible across memory regions, enhancing a static variable with
164//! region-local caching to ensure low latency and high memory throughput for read operations.
165//!
166//! The [`region_local`][6] crate provides a similar mechanism but limits the visibility of values
167//! to only a single memory region - updates do not propagate across region boundaries. This may be
168//! a useful alternative if you want unique values per memory region, similar to `thread_local_rc!`.
169//!
170//! [1]: crate::RegionCachedExt
171//! [2]: crate::region_cached
172//! [3]: linked
173//! [4]: linked::InstancePerThread
174//! [5]: crate::RegionCached
175//! [6]: https://docs.rs/region_local/latest/region_local/
176//! [7]: linked::InstancePerThreadSync
177
178use simple_mermaid::mermaid;
179
180mod macros;
181mod region_cached;
182mod region_cached_ext;
183
184pub use region_cached::*;
185pub use region_cached_ext::*;
186
187/// Macros require these things to be public but they are not part of the public API.
188#[doc(hidden)]
189pub mod __private;