Skip to main content

rumtk_arena/
cpu.rs

1/*
2 *     rumtk attempts to implement HL7 and medical protocols for interoperability in medicine.
3 *     This toolkit aims to be reliable, simple, performant, and standards compliant.
4 *     Copyright (C) 2026  Luis M. Santos, M.D. <lsantos@medicalmasses.com>
5 *     Copyright (C) 2026  MedicalMasses L.L.C. <contact@medicalmasses.com>
6 *
7 *     This program is free software: you can redistribute it and/or modify
8 *     it under the terms of the GNU General Public License as published by
9 *     the Free Software Foundation, either version 3 of the License, or
10 *     (at your option) any later version.
11 *
12 *     This program is distributed in the hope that it will be useful,
13 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
14 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 *     GNU General Public License for more details.
16 *
17 *     You should have received a copy of the GNU General Public License
18 *     along with this program.  If not, see <https://www.gnu.org/licenses/>.
19 */
20use crate::base::RUMVec;
21pub use branches::{likely as cpu_likely_branch, prefetch_read_data, unlikely as cpu_unlikely_branch};
22pub use std::simd::prelude::*;
23use crate::rumtk_mem_quick_array_init;
24
25pub const CPU_L1_PREFETCH: i32 = 0;
26pub const CPU_L2_PREFETCH: i32 = 1;
27pub const CPU_L3_PREFETCH: i32 = 2;
28pub const CPU_NONTEMPORAL_PREFETCH: i32 = 3;
29pub const CPU_L1_CACHE_LINE_SIZE: usize = 64; // Number of bytes in a typical x86_64 CPU L1 cache line.
30pub const CPU_L1_CACHE_SIZE: usize = 32 * 1024; // Number of bytes in a typical x86_64 CPU L1 cache per core.
31pub const CPU_PAGE_SIZE: usize = 4 * 1024; // Typical CPU page size
32pub const CPU_SIMD_64_SIZE: usize = 64;
33pub const CPU_SIMD_32_SIZE: usize = 32;
34pub const CPU_SIMD_16_SIZE: usize = 16;
35pub const CPU_SIMD_8_SIZE: usize = 8;
36pub const CPU_SIMD_AVERAGE_ALU_COUNT: usize = 4;
37pub const CPU_SEARCH_WINDOW_1024_SIZE: usize = 1024;
38pub const CPU_SEARCH_WINDOW_512_SIZE: usize = 512;
39pub const CPU_SEARCH_WINDOW_256_SIZE: usize = 256;
40pub const CPU_SEARCH_WINDOW_128_SIZE: usize = 128;
41pub const CPU_SEARCH_WINDOW_64_SIZE: usize = 64;
42pub const CPU_SEARCH_WINDOW_32_SIZE: usize = 32;
43pub const CPU_SEARCH_WINDOW_16_SIZE: usize = 16;
44
45
46#[cfg(feature = "simd")]
47pub type u8xN<const SEARCH_WINDOW_SIZE: usize> = Simd<u8, SEARCH_WINDOW_SIZE>;
48
49////////////////////////////////////////CPU CACHE HINTS///////////////////////////////
50#[inline]
51pub fn cpu_l3_prefetch(data: *const u8) {
52    prefetch_read_data::<u8, CPU_L3_PREFETCH>(data);
53}
54
55#[inline]
56pub fn cpu_l2_prefetch(data: *const u8) {
57    prefetch_read_data::<u8, CPU_L2_PREFETCH>(data);
58}
59
60#[inline]
61pub fn cpu_l1_prefetch(data: *const u8) {
62    prefetch_read_data::<u8, CPU_L1_PREFETCH>(data);
63}
64
65#[inline(always)]
66pub fn cpu_slice_to_array<const SLICE_SIZE: usize>(chunk: &[u8]) -> [u8; SLICE_SIZE] {
67    chunk.try_into().expect("length mismatch")
68}
69
70#[inline(always)]
71pub fn cpu_slice_to_array_padded<const SLICE_SIZE: usize, const PAD: u8>(chunk: &[u8]) -> [u8; SLICE_SIZE] {
72    let mut result = [PAD; SLICE_SIZE];
73    result[..chunk.len()].copy_from_slice(chunk);
74    result
75}
76
77#[cfg(feature = "simd")]
78#[inline(always)]
79pub fn cpu_slice_to_simd<const SLICE_SIZE: usize>(chunk: &[u8]) -> u8xN<SLICE_SIZE> {
80    u8xN::from_slice(chunk)
81}
82
83#[cfg(feature = "simd")]
84#[inline(always)]
85pub fn cpu_slice_to_simd_padded<const SLICE_SIZE: usize, const PAD: u8>(chunk: &[u8]) -> u8xN<SLICE_SIZE> {
86    u8xN::from_array(cpu_slice_to_array_padded::<SLICE_SIZE, 0>(chunk))
87}
88
89#[inline(always)]
90pub fn cpu_slice_splat<const SLICE_SIZE: usize>(input: &[u8]) -> [u8; SLICE_SIZE] {
91    let mut result = [0u8; SLICE_SIZE];
92
93    for chunk in result.chunks_mut(input.len()) {
94        chunk.copy_from_slice(&input[..chunk.len()]);
95    }
96
97    result
98}
99
100////////////////////////////////////////SIMD MASKS//////////////////////////////////////////
101#[cfg(feature = "simd")]
102#[inline(always)]
103pub fn cpu_simd_shift_right_n<const LANE_SIZE: usize, const SHIFT: usize, const PAD: u8>(item: &u8xN<LANE_SIZE>) -> u8xN<LANE_SIZE> {
104    item.shift_elements_right::<SHIFT>(PAD)
105}
106
107#[cfg(feature = "simd")]
108#[inline(always)]
109pub fn cpu_simd_masks<const LANE_SIZE: usize>(pattern: &[u8]) -> RUMVec<u8xN<LANE_SIZE>> {
110    let mask = u8xN::<LANE_SIZE>::from_array(cpu_slice_splat(pattern));
111    let mut masks = RUMVec::<u8xN<LANE_SIZE>>::with_capacity(pattern.len());
112
113    masks.push(mask);
114
115    for i in 1..pattern.len() {
116        let shifted = cpu_simd_shift_right_n::<LANE_SIZE, 1, 0>(&mask);
117        masks.push(shifted);
118    }
119
120    masks
121}
122
123////////////////////////////////////////SEARCH FOR NEEDLE IN HAYSTACK///////////////////////////////
124
125#[inline(always)]
126pub fn cpu_find_fallback(chunk: &[u8], byte: u8) -> Option<usize> {
127    chunk.iter().position(|c| *c==byte)
128}
129
130#[cfg(feature = "simd")]
131#[inline(always)]
132fn cpu_find_simd_avx2_n<const SEARCH_WINDOW_SIZE: usize>(data_vec: u8xN<SEARCH_WINDOW_SIZE>, target: u8xN<SEARCH_WINDOW_SIZE>) -> Option<usize> {
133    let mask = data_vec.simd_eq(target);
134
135    if mask.any() {
136        let bitmask = mask.to_bitmask();
137        let lane_i = bitmask.trailing_zeros() as usize;
138        return Some(lane_i);
139    }
140
141    None
142}
143
144#[cfg(feature = "simd")]
145#[inline(always)]
146fn cpu_find_simd_avx2_unpadded<const SEARCH_WINDOW_SIZE: usize>(chunk: &[u8], target: u8xN<SEARCH_WINDOW_SIZE>) -> Option<usize> {
147    let data_vec = cpu_slice_to_simd::<SEARCH_WINDOW_SIZE>(chunk);
148    cpu_find_simd_avx2_n(data_vec, target)
149}
150
151#[cfg(feature = "simd")]
152#[inline(always)]
153fn cpu_find_simd_avx2_padded<const SEARCH_WINDOW_SIZE: usize>(chunk: &[u8], target: u8xN<SEARCH_WINDOW_SIZE>) -> Option<usize> {
154    let data_vec = cpu_slice_to_simd_padded::<SEARCH_WINDOW_SIZE, 0>(chunk);
155    cpu_find_simd_avx2_n(data_vec, target)
156}
157
158///
159/// Use SIMD to find the index of a byte in a byte slice.
160///
161///
162#[cfg(feature = "simd")]
163#[inline(always)]
164pub fn cpu_find_simd_n<const LANE_SIZE: usize>
165(
166    chunk: &[u8],
167    byte: u8,
168) -> Option<usize>
169{
170    let mask = u8xN::<LANE_SIZE>::splat(byte);
171    let needs_padding = chunk.len() % LANE_SIZE;
172    let unpadded = chunk.len() - needs_padding; // The compiler will optimize this with a constant per my compiler explorer experiment
173    // https://godbolt.org/z/fhh5nG34f
174
175    for (i,window) in chunk[..unpadded].chunks(LANE_SIZE).enumerate() {
176        cpu_l1_prefetch(window.as_ptr());
177        if let Some(lane_i) = cpu_find_simd_avx2_unpadded::<LANE_SIZE>(window, mask) {
178            return Some(i * LANE_SIZE + lane_i);
179        }
180    }
181
182    cpu_find_simd_avx2_padded::<LANE_SIZE>(&chunk[unpadded..], mask).map(|idx| unpadded + idx)
183}
184
185#[cfg(feature = "simd")]
186#[inline(always)]
187pub fn cpu_find(window: &[u8], byte: u8) -> Option<usize> {
188    cpu_find_simd_n::<CPU_SIMD_64_SIZE>(
189        window,
190        byte,
191    )
192}
193
194#[cfg(not(feature = "simd"))]
195#[inline]
196pub fn cpu_find(window: &[u8], byte: u8) -> Option<usize> {
197    cpu_find_fallback(
198        window,
199        byte,
200    )
201}
202
203////////////////////////////////////////SEARCH FOR CONTINUOUS SLICE OF NEEDLES///////////////////////////////
204#[inline(always)]
205pub fn cpu_continuous_count_fallback(chunk: &[u8], byte: u8) -> Option<usize> {
206    let mut count = 0;
207    for i in 0..chunk.len() {
208        if cpu_unlikely_branch(chunk[i] != byte) {
209            return Some(count);
210        }
211        count += 1;
212    }
213    Some(count)
214}
215
216#[cfg(feature = "simd")]
217#[inline]
218fn cpu_continuous_count_simd_avx2_n<const SEARCH_WINDOW_SIZE: usize>(data_vec: u8xN<SEARCH_WINDOW_SIZE>, target: u8xN<SEARCH_WINDOW_SIZE>) -> usize {
219    let mask = data_vec.simd_eq(target);
220
221    let bitmask = mask.to_bitmask();
222    bitmask.trailing_zeros() as usize
223}
224
225#[cfg(feature = "simd")]
226#[inline]
227fn cpu_continuous_count_simd_avx2_unpadded<const SEARCH_WINDOW_SIZE: usize>(chunk: &[u8], target: u8xN<SEARCH_WINDOW_SIZE>) -> usize {
228    let data_vec = cpu_slice_to_simd::<SEARCH_WINDOW_SIZE>(chunk);
229    cpu_continuous_count_simd_avx2_n(data_vec, target)
230}
231
232#[cfg(feature = "simd")]
233#[inline]
234fn cpu_continuous_count_simd_avx2_padded<const SEARCH_WINDOW_SIZE: usize>(chunk: &[u8], target: u8xN<SEARCH_WINDOW_SIZE>) -> usize {
235    let data_vec = cpu_slice_to_simd_padded::<SEARCH_WINDOW_SIZE, 0>(chunk);
236    cpu_continuous_count_simd_avx2_n(data_vec, target)
237}
238
239///
240/// Use SIMD to find the index of a byte in a byte slice.
241///
242/// ## Note
243///
244/// We try saturating the CPU SIMD ports with ops. If we had to do all [CPU_SIMD_AVERAGE_ALU_COUNT]
245/// passes to find the needle, we essentially speculatively precomputed the index. Our worst case
246/// is if we only had to do one pass to find the needle, but we are relying on out-of-order execution
247/// and the cheapness of SIMD to hide the latency. This also serves as a form of prefetching the
248/// byte slice into the cache lines, so beware of thrashing it.
249///
250///
251#[cfg(feature = "simd")]
252#[inline]
253pub fn cpu_continuous_count_simd_n<const LANE_SIZE: usize>
254(
255    chunk: &[u8],
256    byte: u8,
257) -> Option<usize>
258{
259    let mask = u8xN::<LANE_SIZE>::splat(byte);
260    let mut iter = chunk.chunks(LANE_SIZE);
261    let max_iter = iter.len();
262    let large_steps = max_iter / CPU_SIMD_AVERAGE_ALU_COUNT; // div here so consider changing to shifts for extra ns perf
263    let mut indx = 0;
264
265    // Try saturating the CPU SIMD ports with ops. If we had to do all 4 passes to find the needle,
266    // we essentially speculatively precomputed the index. Our worst case is if we only had to do
267    // one pass to find the needle but we are relying on out of order execution and the cheapness of
268    // SIMD to hide the latency. This also serves as a form of prefetching byte slice.
269    for _ in 0..large_steps {
270        let mut sizes = rumtk_mem_quick_array_init!(usize, CPU_SIMD_AVERAGE_ALU_COUNT);
271        sizes[0] = cpu_continuous_count_simd_avx2_unpadded::<LANE_SIZE>(iter.next().unwrap_or_default(), mask);
272        sizes[1] = cpu_continuous_count_simd_avx2_unpadded::<LANE_SIZE>(iter.next().unwrap_or_default(), mask);
273        sizes[2] = cpu_continuous_count_simd_avx2_unpadded::<LANE_SIZE>(iter.next().unwrap_or_default(), mask);
274        sizes[3] = cpu_continuous_count_simd_avx2_unpadded::<LANE_SIZE>(iter.next().unwrap_or_default(), mask);
275
276        for s in sizes {
277            if s < LANE_SIZE {
278                return Some(indx + s);
279            }
280            indx += LANE_SIZE;
281        }
282    }
283
284    match iter.next() {
285        Some(window) => {
286            Some(indx + cpu_continuous_count_simd_avx2_padded::<LANE_SIZE>(window, mask))
287        },
288        None => Some(indx),
289    }
290}
291
292#[cfg(feature = "simd")]
293#[inline]
294pub fn cpu_continuous_count(window: &[u8], byte: u8) -> Option<usize> {
295    let start = cpu_find(window, byte)?;
296    cpu_continuous_count_simd_n::<CPU_SIMD_64_SIZE>(
297        &window[start..],
298        byte,
299    )
300}
301
302#[cfg(not(feature = "simd"))]
303#[inline]
304pub fn cpu_continuous_count(window: &[u8], byte: u8) -> Option<usize> {
305    let start = cpu_find(window, byte)?;
306    cpu_continuous_count_fallback(
307        &window[start..],
308        byte,
309    )
310}
311
312/////////////////////////////Replacement Helpers///////////////////////////////
313#[inline(always)]
314pub fn cpu_replace_fallback(data: &mut [u8], pattern: u8, replacement: u8) {
315    for i in 0..data.len() {
316        if data[i] == pattern {
317            data[i] = replacement;
318        }
319    }
320}
321
322#[cfg(feature = "simd")]
323#[inline(always)]
324pub fn cpu_find_replace_simd_n<const LANE_SIZE: usize>(chunk: &mut [u8], pattern: u8xN<LANE_SIZE>, replacement: u8xN<LANE_SIZE>) {
325    let simd_chunk = u8xN::<LANE_SIZE>::from_array(cpu_slice_to_array_padded::<LANE_SIZE, 0>(chunk));
326    let bitmask = simd_chunk.simd_eq(pattern);
327
328    if bitmask.any() {
329        replacement.store_select(chunk, bitmask);
330    }
331}
332
333#[cfg(feature = "simd")]
334#[inline(always)]
335pub fn cpu_replace_simd_n<const LANE_SIZE: usize>(data: &mut [u8], pattern: u8, replacement: u8) {
336    let mask = u8xN::<LANE_SIZE>::splat(pattern);
337    let simd_replacement = u8xN::<LANE_SIZE>::splat(replacement);
338
339    for chunk in data.chunks_mut(LANE_SIZE) {
340        cpu_find_replace_simd_n::<LANE_SIZE>(chunk, mask, simd_replacement);
341    }
342}
343
344#[cfg(feature = "simd")]
345#[inline(always)]
346pub fn cpu_replace_byte(data: &mut [u8], pattern: u8, replacement: u8) {
347    cpu_replace_simd_n::<CPU_SIMD_64_SIZE>(data, pattern, replacement)
348}
349
350#[cfg(not(feature = "simd"))]
351#[inline(always)]
352pub fn cpu_replace_byte(data: &mut [u8], pattern: u8, replacement: u8) {
353    cpu_replace_fallback(data, pattern, replacement)
354}
355
356/////////////////////////////GATHER ALL INDICES OF NEEDLE IN HAYSTACK///////////////////////////////
357pub type CPUTokenStackIndex<const LANE_SIZE: usize> = [u32; LANE_SIZE];
358pub type CPUTokenRelativeStackInfo<const LANE_SIZE: usize> = (usize, CPUTokenStackIndex<LANE_SIZE>);
359pub type CPUTokenIndexCollection = RUMVec<u32>;
360pub type CPUTokenIndexSet = (u8, RUMVec<u32>);
361pub type CPUTokenSet = (u8, u32);
362pub type CPUTokenSetCollection = RUMVec<CPUTokenSet>;
363
364#[inline(always)]
365pub fn cpu_collect_fallback(chunk: &[u8], byte: u8, offset: usize) -> CPUTokenIndexCollection {
366    let mut results: CPUTokenStackIndex<CPU_SIMD_64_SIZE> = [0; CPU_SIMD_64_SIZE];
367    let mut length = 0;
368
369    for i in 0..chunk.len() {
370        if chunk[i]==byte {
371            let pos = offset + i;
372            results[length] = pos as u32;
373            length += 1;
374        }
375    }
376
377    CPUTokenIndexCollection::from(&results[..length])
378}
379
380#[cfg(feature = "simd")]
381#[inline]
382fn cpu_collect_simd_avx2_n<const LANE_SIZE: usize>(data_vec: &u8xN<LANE_SIZE>, target: u8xN<LANE_SIZE>, offset: usize) -> Option<CPUTokenRelativeStackInfo<LANE_SIZE>> {
383    let mut results: CPUTokenStackIndex<LANE_SIZE> = [0; LANE_SIZE];
384    let mut length = 0;
385
386    let mask = data_vec.simd_eq(target);
387
388    if mask.any() {
389        let items = mask.to_array();
390
391        for i in 0..items.len() {
392            if items[i] {
393                let pos = offset + i;
394                results[length] = pos as u32;
395                length += 1;
396            }
397        }
398
399        return Some((length, results));
400    }
401
402    None
403}
404
405#[cfg(feature = "simd")]
406#[inline]
407pub fn cpu_collect_simd_n<const LANE_SIZE: usize>
408(
409    chunk: &[u8],
410    byte: u8,
411    offset: usize
412) -> CPUTokenIndexCollection
413{
414    let mask = u8xN::<LANE_SIZE>::splat(byte);
415    let (prefix, middle, postfix) = chunk.as_simd::<LANE_SIZE>();
416
417    let mut local_offset: usize = offset;
418    let data: CPUTokenIndexCollection = cpu_collect_fallback(prefix, byte, local_offset);
419    let mut positions: CPUTokenIndexCollection = CPUTokenIndexCollection::from(data);
420    local_offset += prefix.len();
421
422    for window in middle.into_iter() {
423        match cpu_collect_simd_avx2_n::<LANE_SIZE>(window, mask, local_offset) {
424            Some((len, data)) => {
425                positions.extend_from_slice(&data[..len]);
426            },
427            None => {},
428        };
429        local_offset += LANE_SIZE;
430    }
431
432    let data: CPUTokenIndexCollection = cpu_collect_fallback(postfix, byte, local_offset);
433    positions.extend_from_slice(&data);
434
435    positions
436}
437
438#[cfg(feature = "simd")]
439#[inline]
440pub fn cpu_collect(window: &[u8], byte: u8, offset: usize) -> CPUTokenIndexSet {
441    let indx = cpu_collect_simd_n::<CPU_SIMD_64_SIZE>(
442        window,
443        byte,
444        offset
445    );
446    (byte, indx)
447}
448
449#[cfg(not(feature = "simd"))]
450#[inline]
451pub fn cpu_collect(window: &[u8], byte: u8, offset: usize) -> CPUTokenIndexSet {
452    let indx = cpu_collect_fallback(
453        window,
454        byte,
455        offset
456    );
457    (byte, indx)
458}
459
460#[inline]
461pub fn cpu_tokenize<const WINDOW_SIZE: usize>(haystack: &[u8], bytes: &[u8]) -> CPUTokenSetCollection
462{
463    let mut results = CPUTokenSetCollection::with_capacity(haystack.len() * size_of::<CPUTokenSet>());
464    let mut offset = 0;
465
466    for window in haystack.chunks(WINDOW_SIZE) {
467        for byte in bytes {
468            let (b, indx) = cpu_collect(window, *byte, offset);
469
470            if !indx.is_empty() {
471                for tok_indx in indx {
472                    results.push((b, tok_indx));
473                }
474            }
475        }
476        offset += window.len();
477    }
478
479    results.sort_unstable_by(|a,b| a.1.cmp(&b.1));
480
481    results
482}
483
484#[inline]
485pub fn cpu_tokenize_rev<const WINDOW_SIZE: usize>(haystack: &[u8], bytes: &[u8]) -> CPUTokenSetCollection
486{
487    let reversed: Vec<u8> = bytes.iter().rev().cloned().collect();
488    cpu_tokenize::<WINDOW_SIZE>(haystack, &reversed)
489}