|
| 1 | +//! Lane S — the SWAR lane: the actual 1BRC-frontier compute levers on top of |
| 2 | +//! the flat open-addressed table (lane F's group-by, which was already right). |
| 3 | +//! |
| 4 | +//! What the real 1BRC winners do that lanes A/C/F/R do NOT (see the answer in |
| 5 | +//! the session log): find delimiters with SWAR (8 bytes at a time in a `u64`) |
| 6 | +//! instead of a byte-by-byte `while data[i] != b';'` loop, and parse the |
| 7 | +//! temperature branchlessly. Those are the two measurable levers here. |
| 8 | +//! |
| 9 | +//! - **(b) SWAR delimiter find** — the classic `haszero` bit trick: |
| 10 | +//! `x = word ^ needle_bcast; (x - 0x0101…) & !x & 0x8080…` sets the high bit |
| 11 | +//! of each byte where `word == needle`. `trailing_zeros() >> 3` gives the |
| 12 | +//! first match position. Replaces the scalar name/temp scans. |
| 13 | +//! - **(b) branchless temp parse** — the field is `-?\d?\d.\d`; parse to fixed |
| 14 | +//! -point tenths with no data-dependent branches beyond the sign/width. |
| 15 | +//! - **(c) name compare** — kept as `&[u8] == &[u8]`, which LLVM already lowers |
| 16 | +//! to a vectorized `memcmp`; a hand-rolled 8-byte-chunk compare is the same |
| 17 | +//! instruction sequence, so this is (c) already. |
| 18 | +//! |
| 19 | +//! **(a) mmap is deliberately NOT here and is NOT measurable in this harness:** |
| 20 | +//! `main.rs` does `fs::read` BEFORE `Instant::now()`, so the `mrows_s` metric is |
| 21 | +//! compute-only. mmap is a wall-clock / 13 GB-allocation lever for the full 1B |
| 22 | +//! file, on an axis this timer does not observe. Claiming an mmap speedup on |
| 23 | +//! `mrows_s` would be false; measuring it needs an end-to-end wall-clock mode. |
| 24 | +//! |
| 25 | +//! Reuses lane F's `SoaTable` (flat open-addressed accumulator) verbatim — the |
| 26 | +//! ONLY variable vs lane F is the scan+parse. Same `chunk_bounds`/`merge_maps` |
| 27 | +//! threading. std-only; keeps the crate's zero-dep contract. |
| 28 | +
|
| 29 | +use crate::lane_f::{fnv1a64, morton_slot, table_to_map, SoaTable}; |
| 30 | +use crate::{chunk_bounds, merge_maps, Stats}; |
| 31 | +use std::collections::BTreeMap; |
| 32 | + |
| 33 | +const SEMI_BCAST: u64 = 0x3B3B_3B3B_3B3B_3B3B; // b';' in every byte |
| 34 | +const NL_BCAST: u64 = 0x0A0A_0A0A_0A0A_0A0A; // b'\n' in every byte |
| 35 | +const ONES: u64 = 0x0101_0101_0101_0101; |
| 36 | +const HIGH: u64 = 0x8080_8080_8080_8080; |
| 37 | + |
| 38 | +/// SWAR "has zero byte" applied to `word ^ bcast`: a set 0x80 in each byte |
| 39 | +/// where `word`'s byte equals `bcast`'s byte. |
| 40 | +#[inline(always)] |
| 41 | +fn match_mask(word: u64, bcast: u64) -> u64 { |
| 42 | + let x = word ^ bcast; |
| 43 | + x.wrapping_sub(ONES) & !x & HIGH |
| 44 | +} |
| 45 | + |
| 46 | +/// First index `>= i` where `data[idx]` equals `bcast`'s byte — SWAR 8 bytes at |
| 47 | +/// a time, scalar tail. Reads only within `data` (the `i + 8 <= len` guard). |
| 48 | +#[inline(always)] |
| 49 | +fn find(data: &[u8], mut i: usize, bcast: u64) -> usize { |
| 50 | + let len = data.len(); |
| 51 | + while i + 8 <= len { |
| 52 | + let word = u64::from_le_bytes(data[i..i + 8].try_into().unwrap()); |
| 53 | + let m = match_mask(word, bcast); |
| 54 | + if m != 0 { |
| 55 | + return i + ((m.trailing_zeros() >> 3) as usize); |
| 56 | + } |
| 57 | + i += 8; |
| 58 | + } |
| 59 | + let b = bcast as u8; |
| 60 | + while i < len && data[i] != b { |
| 61 | + i += 1; |
| 62 | + } |
| 63 | + i |
| 64 | +} |
| 65 | + |
| 66 | +/// Parse `-?\d?\d.\d` to fixed-point tenths. Branch only on sign and 1-vs-2 |
| 67 | +/// integer digits (both perfectly predicted at ~50/50 and ~90/10). |
| 68 | +#[inline(always)] |
| 69 | +fn parse_tenths(s: &[u8]) -> i32 { |
| 70 | + let neg = s[0] == b'-'; |
| 71 | + let d = if neg { &s[1..] } else { s }; |
| 72 | + let v = if d.len() == 3 { |
| 73 | + // d.d |
| 74 | + (d[0] - b'0') as i32 * 10 + (d[2] - b'0') as i32 |
| 75 | + } else { |
| 76 | + // dd.d |
| 77 | + (d[0] - b'0') as i32 * 100 + (d[1] - b'0') as i32 * 10 + (d[3] - b'0') as i32 |
| 78 | + }; |
| 79 | + if neg { |
| 80 | + -v |
| 81 | + } else { |
| 82 | + v |
| 83 | + } |
| 84 | +} |
| 85 | + |
| 86 | +/// Scan `data` with the SWAR find + branchless parse, folding into lane F's |
| 87 | +/// flat `SoaTable`. The ONLY difference from `lane_f::accumulate_table` is the |
| 88 | +/// scan/parse — same hash, same slot fn, same table. |
| 89 | +fn accumulate_swar(data: &[u8]) -> SoaTable { |
| 90 | + let mut table = SoaTable::new(); |
| 91 | + let len = data.len(); |
| 92 | + let mut i = 0usize; |
| 93 | + while i < len { |
| 94 | + let name_start = i; |
| 95 | + let semi = find(data, i, SEMI_BCAST); |
| 96 | + let name = &data[name_start..semi]; |
| 97 | + let nl = find(data, semi + 1, NL_BCAST); |
| 98 | + let tenths = parse_tenths(&data[semi + 1..nl]); |
| 99 | + let h = fnv1a64(name); |
| 100 | + table.observe(morton_slot(h), h, name, tenths); |
| 101 | + i = nl + 1; |
| 102 | + } |
| 103 | + table |
| 104 | +} |
| 105 | + |
| 106 | +/// Lane S — SWAR delimiter scan + branchless parse over lane F's flat table. |
| 107 | +pub fn lane_s_swar(data: &[u8], workers: usize) -> BTreeMap<String, Stats> { |
| 108 | + let workers = workers.max(1); |
| 109 | + let bounds = chunk_bounds(data, workers); |
| 110 | + let results: Vec<BTreeMap<String, Stats>> = std::thread::scope(|scope| { |
| 111 | + let handles: Vec<_> = bounds |
| 112 | + .iter() |
| 113 | + .map(|&(start, end)| { |
| 114 | + let slice = &data[start..end]; |
| 115 | + scope.spawn(move || table_to_map(accumulate_swar(slice))) |
| 116 | + }) |
| 117 | + .collect(); |
| 118 | + handles |
| 119 | + .into_iter() |
| 120 | + .map(|h| h.join().expect("lane S worker panicked")) |
| 121 | + .collect() |
| 122 | + }); |
| 123 | + merge_maps(results) |
| 124 | +} |
| 125 | + |
| 126 | +#[cfg(test)] |
| 127 | +mod tests { |
| 128 | + use super::*; |
| 129 | + |
| 130 | + #[test] |
| 131 | + fn swar_find_matches_scalar() { |
| 132 | + let data = b"hello;12.3\nab;-4.5\n"; |
| 133 | + // ';' after "hello" (idx 5), '\n' after "12.3" (idx 10), etc. |
| 134 | + assert_eq!(find(data, 0, SEMI_BCAST), 5); |
| 135 | + assert_eq!(find(data, 6, NL_BCAST), 10); |
| 136 | + assert_eq!(find(data, 11, SEMI_BCAST), 13); |
| 137 | + assert_eq!(find(data, 14, NL_BCAST), 18); |
| 138 | + } |
| 139 | + |
| 140 | + #[test] |
| 141 | + fn parse_tenths_all_shapes() { |
| 142 | + assert_eq!(parse_tenths(b"1.0"), 10); |
| 143 | + assert_eq!(parse_tenths(b"12.3"), 123); |
| 144 | + assert_eq!(parse_tenths(b"-4.5"), -45); |
| 145 | + assert_eq!(parse_tenths(b"-99.9"), -999); |
| 146 | + assert_eq!(parse_tenths(b"0.0"), 0); |
| 147 | + } |
| 148 | + |
| 149 | + #[test] |
| 150 | + fn lane_s_agrees_with_lane_a() { |
| 151 | + let dir = std::env::temp_dir(); |
| 152 | + let path = dir.join(format!("onebrc_probe_test_s_{}.txt", std::process::id())); |
| 153 | + let result = crate::gen::gen(&path, 50_000, 97).expect("gen"); |
| 154 | + assert_eq!(result.rows, 50_000); |
| 155 | + let data = std::fs::read(&path).expect("read generated corpus"); |
| 156 | + std::fs::remove_file(&path).ok(); |
| 157 | + |
| 158 | + let a = crate::lane_a_scalar(&data); |
| 159 | + let s = lane_s_swar(&data, 3); |
| 160 | + assert_eq!(a, s, "SWAR lane must produce identical aggregates to lane A"); |
| 161 | + assert!(!a.is_empty()); |
| 162 | + } |
| 163 | +} |
0 commit comments