Skip to main content

crossfire_utils/
backoff.rs

1use core::mem::transmute;
2use core::sync::atomic::{AtomicBool, AtomicU32, Ordering};
3
4pub const SPIN_LIMIT: u16 = 6;
5
6#[cfg(target_arch = "x86_64")]
7pub const DEFAULT_LIMIT: u16 = 6;
8#[cfg(not(target_arch = "x86_64"))]
9pub const DEFAULT_LIMIT: u16 = 10;
10pub const MAX_LIMIT: u16 = 10;
11
12pub const DEFAULT_CONFIG: u32 =
13    BackoffConfig { spin_limit: SPIN_LIMIT, limit: DEFAULT_LIMIT }.to_u32();
14
15static DETECT_CONFIG: AtomicU32 = AtomicU32::new(DEFAULT_CONFIG);
16
17static _INIT: AtomicBool = AtomicBool::new(false);
18
19/// Detect cpu number and auto setting backoff config.
20///
21/// On one core system, it will be more effective (as much as 2x faster) to use yield than spinning.
22///
23/// The function need to be invoke manually in your initialization code, which does not interrupt
24/// channel operation on other thread. By saving the result to global atomic, the effect will apply after execution.
25///
26/// The result we choose not to include this in default channel initialization code, because
27/// Cpu detection process is somehow slow for benchmark standard,
28/// and `thread::available_parallelism()` might require I/O on system files, you may not
29/// like it in sandbox scenario.
30pub fn detect_backoff_cfg() {
31    if _INIT.compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed).is_err() {
32        return;
33    }
34    #[cfg(feature = "std")]
35    {
36        use core::num::NonZero;
37        if std::thread::available_parallelism().unwrap_or(NonZero::new(1).unwrap())
38            == NonZero::new(1).unwrap()
39        {
40            // For one core (like VM machine), better use yield_now instead of spin_loop.
41            DETECT_CONFIG.store(
42                BackoffConfig { spin_limit: 0, limit: DEFAULT_LIMIT }.to_u32(),
43                Ordering::Release,
44            );
45        }
46    }
47}
48
49#[derive(Debug, Clone, Copy)]
50#[repr(C)]
51pub struct BackoffConfig {
52    pub spin_limit: u16,
53    pub limit: u16,
54}
55
56impl Default for BackoffConfig {
57    #[inline(always)]
58    fn default() -> Self {
59        Self::from_u32(DEFAULT_CONFIG)
60    }
61}
62
63impl BackoffConfig {
64    #[inline(always)]
65    pub fn detect() -> Self {
66        Self::from_u32(DETECT_CONFIG.load(Ordering::Relaxed))
67    }
68
69    #[inline(always)]
70    pub const fn to_u32(self) -> u32 {
71        let i: u32 = unsafe { transmute(self) };
72        i
73    }
74
75    #[inline(always)]
76    pub const fn from_u32(config: u32) -> Self {
77        unsafe { transmute(config) }
78    }
79
80    #[allow(dead_code)]
81    #[inline(always)]
82    pub const fn async_limit(mut self, limit: u16) -> Self {
83        if limit < self.limit {
84            self.limit = limit;
85        }
86        self.spin_limit = limit;
87        self
88    }
89
90    #[allow(dead_code)]
91    #[inline(always)]
92    pub const fn limit(mut self, limit: u16) -> Self {
93        self.limit = limit;
94        self
95    }
96
97    #[allow(dead_code)]
98    #[inline(always)]
99    pub const fn spin(mut self, spin_limit: u16) -> Self {
100        if spin_limit < self.spin_limit {
101            self.spin_limit = spin_limit;
102        }
103        self
104    }
105}
106
107pub struct Backoff {
108    step: u16,
109    pub config: BackoffConfig,
110}
111
112impl Backoff {
113    #[inline(always)]
114    pub fn new() -> Self {
115        Self { step: 0, config: BackoffConfig::default() }
116    }
117
118    #[inline(always)]
119    pub fn from(config: BackoffConfig) -> Self {
120        Self { step: 0, config }
121    }
122
123    #[allow(dead_code)]
124    #[inline(always)]
125    pub fn spin(&mut self) -> bool {
126        let step = if self.step < SPIN_LIMIT { self.step } else { SPIN_LIMIT };
127        for _ in 0..1 << step {
128            core::hint::spin_loop();
129        }
130        if self.step < MAX_LIMIT {
131            self.step += 1;
132            self.step >= self.config.limit
133        } else {
134            true
135        }
136    }
137
138    #[inline(always)]
139    pub fn set_step(&mut self, step: u16) {
140        self.step = step;
141    }
142
143    #[inline(always)]
144    pub fn snooze(&mut self) -> bool {
145        if self.step < self.config.limit {
146            self.step += 1;
147            if self.step < self.config.spin_limit {
148                for _ in 0..1 << self.step {
149                    core::hint::spin_loop();
150                }
151                false
152            } else {
153                #[cfg(feature = "std")]
154                {
155                    std::thread::yield_now();
156                    false
157                }
158                #[cfg(not(feature = "std"))]
159                {
160                    true
161                }
162            }
163        } else {
164            true
165        }
166    }
167
168    #[cfg(feature = "std")]
169    #[inline(always)]
170    pub fn yield_now(&mut self) -> bool {
171        std::thread::yield_now();
172        if self.step < self.config.limit {
173            self.step += 1;
174            self.step >= self.config.limit
175        } else {
176            true
177        }
178    }
179
180    #[inline(always)]
181    pub fn is_completed(&self) -> bool {
182        self.step >= self.config.limit
183    }
184
185    #[inline(always)]
186    pub fn step(&self) -> usize {
187        self.step as usize
188    }
189
190    #[inline(always)]
191    pub fn reset(&mut self) {
192        self.step = 0;
193    }
194}
195
196#[cfg(test)]
197mod tests {
198
199    use super::*;
200
201    #[test]
202    fn test_backoff() {
203        let backoff = Backoff::from(BackoffConfig { spin_limit: 1, limit: 0 });
204        assert!(backoff.is_completed());
205        println!("Option<backoff> size {}", size_of::<Option<Backoff>>());
206        println!("backoff size {}", size_of::<Backoff>());
207        println!("BackoffConfig size {}", size_of::<BackoffConfig>());
208        assert_eq!(size_of::<BackoffConfig>(), size_of::<u32>());
209        let config = BackoffConfig { spin_limit: 6, limit: 7 };
210        let config_i = config.to_u32();
211        let _config = BackoffConfig::from_u32(config_i);
212        assert_eq!(config.spin_limit, _config.spin_limit);
213        assert_eq!(config.limit, _config.limit);
214
215        let mut backoff = Backoff::from(BackoffConfig { spin_limit: 2, limit: 4 });
216        assert_eq!(backoff.step, 0);
217        backoff.spin();
218        assert_eq!(backoff.step, 1);
219        backoff.snooze();
220        assert_eq!(backoff.step, 2);
221        backoff.snooze();
222        backoff.snooze();
223        backoff.snooze();
224        backoff.snooze();
225        assert_eq!(backoff.step, 4);
226        backoff.spin();
227        assert_eq!(backoff.step, 5);
228    }
229}