Skip to content

Commit 427e63e

Browse files
authored
Merge pull request #637 from AdaWorldAPI/claude/onebrc-lane-s-swar
onebrc-probe: lane S — SWAR delimiter scan + branchless temp parse (+23% v3, +31% native)
2 parents e4bea83 + 3b62b78 commit 427e63e

3 files changed

Lines changed: 170 additions & 4 deletions

File tree

crates/onebrc-probe/src/lane_s.rs

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
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+
}

crates/onebrc-probe/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ pub mod lane_j;
6767
#[cfg(feature = "presets")]
6868
pub mod presets;
6969
pub mod sha256;
70+
pub mod lane_s;
7071

7172
#[cfg(feature = "lane-b")]
7273
pub use lane_b::lane_b_simd;
@@ -75,6 +76,7 @@ pub use lane_d::lane_d_ractor;
7576
#[cfg(feature = "lane-e")]
7677
pub use lane_e::lane_e_kanban;
7778
pub use lane_f::{lane_f_morton, lane_r_radix};
79+
pub use lane_s::lane_s_swar;
7880
#[cfg(feature = "lane-g")]
7981
pub use lane_g::{lane_g_kanban_soa, lane_g_kanban_soa_with_morsel};
8082
#[cfg(feature = "lane-h")]

crates/onebrc-probe/src/main.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
//!
44
//! ```text
55
//! onebrc-probe gen <path> <rows> <seed>
6-
//! onebrc-probe run <path> <lane:a|b|c|d|e|f|r> [workers] [batches]
6+
//! onebrc-probe run <path> <lane:a|b|c|d|e|f|r|s> [workers] [batches]
77
//! ```
88
//!
99
//! Lane `b` requires `--features lane-b`; lane `d` and lane `e` require
@@ -23,7 +23,7 @@ fn main() {
2323
Some("run") => cmd_run(&args[2..]),
2424
_ => {
2525
eprintln!(
26-
"usage:\n onebrc-probe gen <path> <rows> <seed>\n onebrc-probe run <path> <lane:a|b|c|d|e|f|r> [workers] [batches]"
26+
"usage:\n onebrc-probe gen <path> <rows> <seed>\n onebrc-probe run <path> <lane:a|b|c|d|e|f|r|s> [workers] [batches]"
2727
);
2828
std::process::exit(2);
2929
}
@@ -55,7 +55,7 @@ fn cmd_gen(args: &[String]) {
5555
fn cmd_run(args: &[String]) {
5656
let path = PathBuf::from(
5757
args.first()
58-
.expect("usage: run <path> <lane:a|b|c|d|e|f|r> [workers] [batches]"),
58+
.expect("usage: run <path> <lane:a|b|c|d|e|f|r|s> [workers] [batches]"),
5959
);
6060
let lane = args.get(1).map(String::as_str).unwrap_or("a");
6161
let workers: usize = args
@@ -132,6 +132,7 @@ fn cmd_run(args: &[String]) {
132132
// Morton-tile SoA lane and its plain-radix control (lane_f.rs).
133133
"f" => onebrc_probe::lane_f_morton(&data, workers),
134134
"r" => onebrc_probe::lane_r_radix(&data, workers),
135+
"s" => onebrc_probe::lane_s_swar(&data, workers),
135136
"g" => {
136137
#[cfg(feature = "lane-g")]
137138
{
@@ -200,7 +201,7 @@ fn cmd_run(args: &[String]) {
200201
}
201202
}
202203
other => {
203-
eprintln!("unknown lane '{other}' (expected 'a', 'b', 'c', 'd', 'e', 'f', or 'r')");
204+
eprintln!("unknown lane '{other}' (expected 'a', 'b', 'c', 'd', 'e', 'f', 'r', or 's')");
204205
std::process::exit(2);
205206
}
206207
};

0 commit comments

Comments
 (0)