use crate::{Adjacency, Dir};
pub const GROUP: u32 = 512;
pub const BLOCK: usize = 32;
#[derive(Debug, Clone, Copy, Default)]
struct Group {
at: u64,
ow: u8,
dw: u8,
base: u32,
nw: u8,
}
#[derive(Debug, Default)]
pub struct Csr {
nodes: u32,
edges: u64,
groups: Vec<Group>,
words: Vec<u64>,
cost: Cost,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Cost {
pub offsets: u64,
pub degrees: u64,
pub firsts: u64,
pub widths: u64,
pub gaps: u64,
pub patches: u64,
pub groups: u64,
pub slack: u64,
}
impl Cost {
#[must_use]
pub fn total(&self) -> u64 {
self.offsets
+ self.degrees
+ self.firsts
+ self.widths
+ self.gaps
+ self.patches
+ self.groups
+ self.slack
}
}
impl Csr {
#[must_use]
pub fn build(nodes: u32, edges: &mut [(u32, u32)]) -> Csr {
assert!(
edges.iter().all(|(s, d)| *s < nodes && *d < nodes),
"an edge names a node outside the graph"
);
edges.sort_unstable();
Csr::encode(nodes, edges)
}
#[must_use]
pub fn from_hot(
hot: &Adjacency,
label: u32,
dir: Dir,
nodes: u32,
id: impl Fn(u64) -> u32,
) -> Csr {
let mut edges = Vec::with_capacity(hot.edges());
hot.for_each_run(label, dir, |node, ns, _| {
let src = id(node);
edges.extend(ns.iter().map(|n| (src, id(*n))));
});
Csr::build(nodes, &mut edges)
}
#[must_use]
pub fn nodes(&self) -> u32 {
self.nodes
}
#[must_use]
pub fn edges(&self) -> u64 {
self.edges
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.edges == 0
}
#[must_use]
pub fn degree(&self, node: u32) -> u32 {
let Some((g, i, count)) = self.locate(node) else {
return 0;
};
let s = self.groups[g];
let run = self.run_at(s, i, count);
read(&self.words, run, s.dw.into()) as u32
}
pub fn neighbours_into(&self, node: u32, out: &mut Vec<u32>) {
out.clear();
let Some((g, i, count)) = self.locate(node) else {
return;
};
let s = self.groups[g];
let mut at = self.run_at(s, i, count);
let deg = read(&self.words, at, s.dw.into()) as usize;
at += u64::from(s.dw);
if deg == 0 {
return;
}
out.reserve(deg);
let mut cur = s.base + read(&self.words, at, s.nw.into()) as u32;
at += u64::from(s.nw);
out.push(cur);
let mut left = deg - 1;
let mut block = [0u32; BLOCK];
while left > 0 {
let n = left.min(BLOCK);
let w = read(&self.words, at, 6) as u32;
at += 6;
let patched = read(&self.words, at, 1) == 1;
at += 1;
let (mut x, mut ew) = (0u64, 0u32);
if patched {
x = read(&self.words, at, POS) + 1;
at += u64::from(POS);
ew = read(&self.words, at, 6) as u32;
at += 6;
}
for slot in &mut block[..n] {
*slot = read(&self.words, at, w) as u32;
at += u64::from(w);
}
for _ in 0..x {
let pos = read(&self.words, at, POS) as usize;
at += u64::from(POS);
let high = read(&self.words, at, ew);
at += u64::from(ew);
block[pos] |= (high << w) as u32;
}
for gap in &block[..n] {
cur += gap;
out.push(cur);
}
left -= n;
}
}
#[must_use]
pub fn neighbours(&self, node: u32) -> Vec<u32> {
let mut out = Vec::new();
self.neighbours_into(node, &mut out);
out
}
pub fn prefetch(&self, node: u32) {
let Some((g, i, _)) = self.locate(node) else {
return;
};
let s = self.groups[g];
let bit = s.at + i * u64::from(s.ow);
yo_common::prefetch(&self.words[(bit / 64) as usize]);
}
#[must_use]
pub fn bytes(&self) -> usize {
self.words.capacity() * size_of::<u64>() + self.groups.capacity() * size_of::<Group>()
}
#[must_use]
pub fn cost(&self) -> Cost {
self.cost
}
#[must_use]
pub fn bits_per_edge(&self) -> f64 {
if self.edges == 0 {
return 0.0;
}
self.bytes() as f64 * 8.0 / self.edges as f64
}
#[inline]
fn locate(&self, node: u32) -> Option<(usize, u64, u64)> {
if node >= self.nodes {
return None;
}
let g = node / GROUP;
let lo = g * GROUP;
Some((
g as usize,
u64::from(node - lo),
u64::from((lo + GROUP).min(self.nodes) - lo),
))
}
#[inline]
fn run_at(&self, s: Group, i: u64, count: u64) -> u64 {
let table = s.at + i * u64::from(s.ow);
s.at + count * u64::from(s.ow) + read(&self.words, table, s.ow.into())
}
fn encode(nodes: u32, edges: &[(u32, u32)]) -> Csr {
let mut w = Writer::default();
let mut groups = Vec::with_capacity(nodes.div_ceil(GROUP) as usize);
let mut runs: Vec<(usize, usize)> = Vec::with_capacity(GROUP as usize);
let mut offs: Vec<u64> = Vec::with_capacity(GROUP as usize);
let mut gaps: Vec<u32> = Vec::new();
let mut e = 0usize;
let mut cost = Cost::default();
for lo in (0..nodes).step_by(GROUP as usize) {
let hi = (lo + GROUP).min(nodes);
let count = (hi - lo) as usize;
runs.clear();
let (mut base, mut top, mut maxdeg) = (u32::MAX, 0u32, 0u32);
for node in lo..hi {
let s = e;
while e < edges.len() && edges[e].0 == node {
e += 1;
}
runs.push((s, e));
maxdeg = maxdeg.max((e - s) as u32);
if e > s {
base = base.min(edges[s].1);
top = top.max(edges[e - 1].1);
}
}
let base = if base == u32::MAX { 0 } else { base };
let dw = width(u64::from(maxdeg));
let nw = width(u64::from(top.saturating_sub(base)));
offs.clear();
let mut total = 0u64;
for (s, t) in &runs {
offs.push(total);
gaps_of(&edges[*s..*t], &mut gaps);
total += run_bits(&gaps, *t > *s, dw, nw);
}
let ow = width(total);
let at = w.bits();
w.skip(count as u64 * u64::from(ow));
cost.offsets += count as u64 * u64::from(ow);
for (i, (s, t)) in runs.iter().enumerate() {
w.put_at(at + i as u64 * u64::from(ow), offs[i], ow);
let run = &edges[*s..*t];
w.put(run.len() as u64, dw);
cost.degrees += u64::from(dw);
if run.is_empty() {
continue;
}
w.put(u64::from(run[0].1 - base), nw);
cost.firsts += u64::from(nw);
gaps_of(run, &mut gaps);
for block in gaps.chunks(BLOCK) {
let p = Plan::best(block);
w.put(u64::from(p.w), 6);
w.put(u64::from(p.x != 0), 1);
cost.widths += 7;
if p.x != 0 {
w.put(u64::from(p.x - 1), POS);
w.put(u64::from(p.ew), 6);
cost.widths += 11;
}
cost.gaps += block.len() as u64 * u64::from(p.w);
for gap in block {
w.put(u64::from(*gap) & mask(p.w), p.w);
}
for (at, gap) in block.iter().enumerate() {
if u64::from(*gap) >> p.w != 0 {
w.put(at as u64, POS);
w.put(u64::from(*gap) >> p.w, p.ew);
cost.patches += u64::from(POS + p.ew);
}
}
}
}
groups.push(Group {
at,
base,
ow: ow as u8,
dw: dw as u8,
nw: nw as u8,
});
}
w.words.push(0);
w.words.shrink_to_fit();
groups.shrink_to_fit();
cost.slack = w.words.capacity() as u64 * 64 - cost.total();
cost.groups = groups.capacity() as u64 * size_of::<Group>() as u64 * 8;
Csr {
nodes,
edges: edges.len() as u64,
groups,
words: w.words,
cost,
}
}
}
fn gaps_of(run: &[(u32, u32)], into: &mut Vec<u32>) {
into.clear();
into.extend(run.windows(2).map(|p| p[1].1 - p[0].1));
}
#[must_use]
pub fn order_by_degree(nodes: u32, edges: &[(u32, u32)]) -> Vec<u32> {
let mut deg = vec![0u32; nodes as usize];
for (s, d) in edges {
deg[*s as usize] += 1;
deg[*d as usize] += 1;
}
let mut order: Vec<u32> = (0..nodes).collect();
order.sort_unstable_by_key(|n| (core::cmp::Reverse(deg[*n as usize]), *n));
let mut to = vec![0u32; nodes as usize];
for (new, old) in order.iter().enumerate() {
to[*old as usize] = new as u32;
}
to
}
pub fn renumber(edges: &mut [(u32, u32)], to: &[u32]) {
for e in edges {
*e = (to[e.0 as usize], to[e.1 as usize]);
}
}
#[derive(Debug, Clone, Copy, Default)]
struct Plan {
w: u32,
x: u32,
ew: u32,
}
const POS: u32 = 5;
impl Plan {
fn best(block: &[u32]) -> Plan {
let mut hist = [0u32; 33];
let mut top = 0u32;
for g in block {
let b = width(u64::from(*g));
hist[b as usize] += 1;
top = top.max(b);
}
let n = block.len() as u64;
let mut best = Plan {
w: top,
x: 0,
ew: 0,
};
let mut cost = 7 + n * u64::from(top);
let mut over = 0u32;
for w in (0..top).rev() {
over += hist[w as usize + 1];
let plan = Plan {
w,
x: over,
ew: top - w,
};
let bits = plan.bits(n);
if bits < cost {
(best, cost) = (plan, bits);
}
}
best
}
fn bits(&self, n: u64) -> u64 {
let head = if self.x == 0 { 7 } else { 7 + 11 };
head + n * u64::from(self.w) + u64::from(self.x) * u64::from(POS + self.ew)
}
}
fn run_bits(gaps: &[u32], any: bool, dw: u32, nw: u32) -> u64 {
if !any {
return u64::from(dw);
}
let mut bits = u64::from(dw) + u64::from(nw);
for block in gaps.chunks(BLOCK) {
bits += Plan::best(block).bits(block.len() as u64);
}
bits
}
#[inline]
fn width(v: u64) -> u32 {
64 - v.leading_zeros()
}
#[inline]
fn mask(w: u32) -> u64 {
if w == 64 { u64::MAX } else { (1u64 << w) - 1 }
}
#[inline]
fn read(words: &[u64], at: u64, w: u32) -> u64 {
if w == 0 {
return 0;
}
let i = (at / 64) as usize;
let off = (at % 64) as u32;
let lo = words[i] >> off;
let got = 64 - off;
if got >= w {
lo & mask(w)
} else {
(lo | (words[i + 1] << got)) & mask(w)
}
}
#[derive(Debug, Default)]
struct Writer {
words: Vec<u64>,
bits: u64,
}
impl Writer {
#[inline]
fn bits(&self) -> u64 {
self.bits
}
fn skip(&mut self, n: u64) {
self.bits += n;
self.room(self.bits);
}
fn put(&mut self, v: u64, w: u32) {
self.put_at(self.bits, v, w);
self.bits += u64::from(w);
}
fn put_at(&mut self, at: u64, v: u64, w: u32) {
if w == 0 {
return;
}
self.room(at + u64::from(w));
let i = (at / 64) as usize;
let off = (at % 64) as u32;
let v = v & mask(w);
debug_assert!(w == 64 || v >> w == 0, "a field wider than it was given");
self.words[i] |= v << off;
if off + w > 64 {
self.words[i + 1] |= v >> (64 - off);
}
}
fn room(&mut self, upto: u64) {
let need = (upto as usize).div_ceil(64) + 1;
if self.words.len() < need {
self.words.resize(need, 0);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use yo_common::Rng;
fn reference(nodes: u32, edges: &[(u32, u32)]) -> Vec<Vec<u32>> {
let mut out = vec![Vec::new(); nodes as usize];
for (s, d) in edges {
out[*s as usize].push(*d);
}
for v in &mut out {
v.sort_unstable();
}
out
}
fn agrees(nodes: u32, mut edges: Vec<(u32, u32)>) -> Csr {
let want = reference(nodes, &edges);
let cold = Csr::build(nodes, &mut edges);
let mut got = Vec::new();
for node in 0..nodes {
cold.neighbours_into(node, &mut got);
assert_eq!(got, want[node as usize], "node {node}");
assert_eq!(
cold.degree(node),
want[node as usize].len() as u32,
"degree of {node}"
);
}
cold
}
fn uniform(nodes: u32, degree: u32, seed: u64) -> Vec<(u32, u32)> {
let mut rng = Rng::new(seed);
let mut edges = Vec::with_capacity((nodes as usize) * (degree as usize));
for src in 0..nodes {
for _ in 0..degree {
edges.push((src, (rng.next_u64() % u64::from(nodes)) as u32));
}
}
edges
}
fn rmat(scale: u32, degree: u32, seed: u64) -> Vec<(u32, u32)> {
let nodes = 1u32 << scale;
let mut rng = Rng::new(seed);
let mut edges = Vec::with_capacity((nodes as usize) * (degree as usize));
for _ in 0..(nodes as u64) * u64::from(degree) {
let (mut r, mut c) = (0u32, 0u32);
for level in 0..scale {
let bit = 1u32 << (scale - 1 - level);
let p = (rng.next_u64() >> 11) as f64 / (1u64 << 53) as f64;
if p < 0.57 {
} else if p < 0.76 {
c |= bit;
} else if p < 0.95 {
r |= bit;
} else {
r |= bit;
c |= bit;
}
}
edges.push((r, c));
}
edges
}
#[test]
fn a_field_comes_back_out_the_way_it_went_in() {
let mut w = Writer::default();
let mut rng = Rng::new(7);
let mut wrote = Vec::new();
for _ in 0..5000 {
let bits = (rng.next_u64() % 33) as u32;
let v = rng.next_u64() & mask(bits);
wrote.push((w.bits(), v, bits));
w.put(v, bits);
}
w.words.push(0);
for (at, v, bits) in wrote {
assert_eq!(read(&w.words, at, bits), v, "at {at} wide {bits}");
}
}
#[test]
fn a_hole_left_on_purpose_can_be_filled_in_later() {
let mut w = Writer::default();
let at = w.bits();
w.skip(40);
w.put(0xabcd, 32);
w.put_at(at, 0x9f_ffff_ffff, 40);
w.words.push(0);
assert_eq!(read(&w.words, at, 40), 0x9f_ffff_ffff);
assert_eq!(read(&w.words, at + 40, 32), 0xabcd);
}
#[test]
fn the_bits_add_up_to_the_bytes() {
let scale = if cfg!(miri) { 8 } else { 12 };
let mut edges = rmat(scale, 8, 0xadd);
let cold = Csr::build(1 << scale, &mut edges);
let c = cold.cost();
assert_eq!(c.total(), cold.bytes() as u64 * 8, "{c:?}");
assert!(
c.gaps > c.offsets,
"the gaps should be the biggest part here"
);
}
#[test]
fn an_empty_graph_is_a_graph() {
let cold = Csr::build(0, &mut []);
assert!(cold.is_empty());
assert_eq!(cold.degree(0), 0);
assert_eq!(cold.neighbours(0), Vec::<u32>::new());
assert_eq!(cold.bits_per_edge(), 0.0);
}
#[test]
fn nodes_with_no_edges_are_still_nodes() {
let cold = agrees(1000, vec![(500, 1), (500, 2)]);
assert_eq!(cold.nodes(), 1000);
assert_eq!(cold.edges(), 2);
assert_eq!(
cold.degree(1000),
0,
"and past the end is nothing rather than a panic"
);
}
#[test]
fn a_run_comes_back_ascending_however_it_went_in() {
agrees(64, vec![(3, 40), (3, 1), (3, 63), (3, 0), (3, 17)]);
}
#[test]
fn parallel_edges_survive_as_the_zero_gaps_they_are() {
let cold = agrees(16, vec![(1, 2), (1, 2), (1, 2), (1, 9)]);
assert_eq!(cold.degree(1), 4);
assert_eq!(cold.neighbours(1), vec![2, 2, 2, 9]);
}
#[test]
fn a_self_loop_is_an_edge_like_any_other() {
agrees(8, vec![(4, 4), (4, 0)]);
}
#[test]
fn the_last_group_can_be_a_partial_one() {
let nodes = GROUP * 2 + 5;
let mut edges = Vec::new();
for src in 0..nodes {
edges.push((src, (src * 7) % nodes));
}
agrees(nodes, edges);
}
#[test]
fn a_hub_spans_as_many_blocks_as_it_needs() {
let hub = if cfg!(miri) { 1_500u32 } else { 200_000 };
let nodes = hub * 5;
let mut edges: Vec<(u32, u32)> = (0..hub).map(|i| (1, i)).collect();
edges.push((1, nodes - 1));
let cold = agrees(nodes, edges);
assert_eq!(cold.degree(1), hub + 1);
}
#[cfg_attr(miri, ignore = "the number of bits an edge is the claim")]
#[test]
fn a_block_of_one_enormous_gap_does_not_price_the_rest() {
let mut edges: Vec<(u32, u32)> = (0..50_000u32).map(|i| (7, i)).collect();
edges.push((7, 99_999));
let cold = Csr::build(100_000, &mut edges.clone());
agrees(100_000, edges);
assert!(
cold.bits_per_edge() < 4.0,
"one far neighbour priced the whole run at {:.2} bits an edge",
cold.bits_per_edge()
);
}
#[test]
fn the_cold_form_agrees_with_a_graph_someone_made_up() {
let mut rng = Rng::new(0x51de);
let (nodes, edges_wanted) = if cfg!(miri) {
(500u32, 6_000)
} else {
(5000, 60_000)
};
let mut edges = Vec::new();
for _ in 0..edges_wanted {
let src = if rng.next_u64().is_multiple_of(10) {
(rng.next_u64() % 20) as u32
} else {
(rng.next_u64() % u64::from(nodes)) as u32
};
edges.push((src, (rng.next_u64() % u64::from(nodes)) as u32));
}
agrees(nodes, edges);
}
#[test]
fn promotion_reads_what_the_hot_plane_holds() {
const FOLLOWS: u32 = 1;
const BLOCKS: u32 = 2;
let mut hot = Adjacency::new();
let mut rng = Rng::new(0x40ce);
let (nodes, wanted) = if cfg!(miri) {
(200u64, 2_000)
} else {
(4000, 40_000)
};
let mut want: Vec<Vec<u32>> = vec![Vec::new(); nodes as usize];
for _ in 0..wanted {
let (s, d) = (rng.next_u64() % nodes, rng.next_u64() % nodes);
hot.link(s, d, FOLLOWS, 0);
want[s as usize].push(d as u32);
}
for _ in 0..wanted / 40 {
hot.link(rng.next_u64() % nodes, rng.next_u64() % nodes, BLOCKS, 0);
}
for v in &mut want {
v.sort_unstable();
}
let cold = Csr::from_hot(&hot, FOLLOWS, Dir::Out, nodes as u32, |n| n as u32);
assert_eq!(cold.edges(), wanted);
let mut got = Vec::new();
for node in 0..nodes as u32 {
cold.neighbours_into(node, &mut got);
assert_eq!(got, want[node as usize], "node {node}");
}
let mut mirror: Vec<Vec<u32>> = vec![Vec::new(); nodes as usize];
for (s, ds) in want.iter().enumerate() {
for d in ds {
mirror[*d as usize].push(s as u32);
}
}
for v in &mut mirror {
v.sort_unstable();
}
let back = Csr::from_hot(&hot, FOLLOWS, Dir::In, nodes as u32, |n| n as u32);
for node in 0..nodes as u32 {
back.neighbours_into(node, &mut got);
assert_eq!(got, mirror[node as usize], "incoming to {node}");
}
}
#[cfg_attr(miri, ignore = "the number of bits an edge is the claim")]
#[test]
fn what_a_random_graph_costs_and_what_a_real_one_saves() {
let nodes = 1u32 << 16;
let degree = 16u32;
let mut random = uniform(nodes, degree, 0xbeef);
let random = Csr::build(nodes, &mut random);
let floor =
((f64::from(nodes) * f64::from(nodes)) / f64::from(nodes * degree)).log2() + 1.44;
let got = random.bits_per_edge();
assert!(
got > floor - 0.5,
"random graph at {got:.2} bits an edge is under its {floor:.2} bit floor, so something is not being counted"
);
assert!(
got < floor * 1.35,
"random graph at {got:.2} bits an edge against a floor of {floor:.2}, so the encoder is wasting a third of itself"
);
let mut social = rmat(16, degree, 0xf00d);
let flat = Csr::build(nodes, &mut social.clone()).bits_per_edge();
assert!(
flat < got - 2.5,
"R-MAT at {flat:.2} bits an edge against uniform at {got:.2}, so having hubs is buying nothing"
);
let to = order_by_degree(nodes, &social);
renumber(&mut social, &to);
let ordered = Csr::build(nodes, &mut social).bits_per_edge();
assert!(
ordered < flat - 2.0,
"degree ordering took R-MAT from {flat:.2} to {ordered:.2} bits an edge, which is not the fifth it was measured at"
);
assert!(
ordered < 10.5,
"R-MAT degree ordered at {ordered:.2} bits an edge, against the 9.89 this was measured at"
);
}
#[cfg_attr(miri, ignore = "the number of bits an edge is the claim")]
#[test]
fn ordering_a_graph_with_no_structure_saves_nothing() {
let nodes = 1u32 << 16;
let mut edges = uniform(nodes, 16, 0xbeef);
let before = Csr::build(nodes, &mut edges.clone()).bits_per_edge();
let to = order_by_degree(nodes, &edges);
renumber(&mut edges, &to);
let after = Csr::build(nodes, &mut edges).bits_per_edge();
assert!(
(after - before).abs() < 0.1,
"degree ordering moved a uniform graph from {before:.2} to {after:.2} bits an edge"
);
}
#[test]
fn a_numbering_is_a_permutation_and_the_graph_survives_it() {
let mut edges = vec![(0u32, 1u32), (0, 2), (0, 3), (5, 0), (5, 1), (9, 0)];
let to = order_by_degree(10, &edges);
let mut seen = to.clone();
seen.sort_unstable();
assert_eq!(seen, (0..10).collect::<Vec<u32>>(), "not a permutation");
assert_eq!(to[0], 0, "the busiest node did not get the smallest id");
let before: Vec<(u32, u32)> = edges.clone();
renumber(&mut edges, &to);
let mapped: Vec<(u32, u32)> = before
.iter()
.map(|(s, d)| (to[*s as usize], to[*d as usize]))
.collect();
assert_eq!(edges, mapped);
let cold = Csr::build(10, &mut edges);
assert_eq!(cold.edges(), 6);
assert_eq!(cold.degree(to[0] as u32), 3);
assert_eq!(cold.degree(to[5] as u32), 2);
}
#[cfg_attr(miri, ignore = "the number of bits an edge is the claim")]
#[test]
fn the_cold_form_is_an_order_of_magnitude_under_the_hot_one() {
let nodes = 1u32 << 16;
let mut edges = rmat(16, 16, 0x0117);
let mut hot = Adjacency::out_only();
for (s, d) in &edges {
hot.link(u64::from(*s), u64::from(*d), 1, 0);
}
hot.compact();
let cold = Csr::build(nodes, &mut edges);
let ratio = hot.bytes() as f64 / cold.bytes() as f64;
assert!(
ratio > 8.0,
"the cold form is only {ratio:.1} times smaller than the hot one, at {:.2} bits an edge against {:.1} bytes",
cold.bits_per_edge(),
hot.bytes() as f64 / hot.edges() as f64
);
}
}