yo_graph/csr.rs
1//! The cold form: the same adjacency at bits an edge rather than bytes.
2//!
3//! [`Adjacency`] is what a graph being written looks like,
4//! twelve bytes an edge and every operation O(1). [`Csr`] is what the settled
5//! part of it looks like once nothing is changing it: read only, node grouped,
6//! and about an order of magnitude smaller. Spec `11` section 2 calls these the
7//! hot form and the cold form and expects a graph to be mostly cold, because a
8//! graph that is being traversed is very rarely being edited at the same rate.
9//!
10//! # The shape
11//!
12//! Nodes are dense `u32` ids, cut into groups of [`GROUP`] consecutive ids.
13//! Everything that varies is chosen per group, so one hub does not set the
14//! width of the whole graph. A group carries:
15//!
16//! - a table of one bit offset per node, at the width that group's stream
17//! needs, pointing at where that node's run starts,
18//! - the runs themselves, each of them a degree, then the first neighbour
19//! written against the group's smallest neighbour, then the gaps between
20//! the rest.
21//!
22//! The gaps go out in blocks of [`BLOCK`] with a width per block rather than a
23//! width per run. That is the one idea from BtrBlocks and FastLanes that is
24//! worth taking here: a run of ten thousand where one gap is enormous and the
25//! rest are small would otherwise pay the enormous one ten thousand times, and
26//! a block only ever pays it thirty two times. It costs seven bits a block,
27//! which is under a quarter of a bit an edge, and no extra offsets at all,
28//! because a run is decoded from its start and the blocks come in order.
29//!
30//! A block whose width does not fit everything in it leaves the ones that did
31//! not fit behind as patches, written again at the end of the block with their
32//! positions, which is what PFOR does. The encoder tries every width and keeps
33//! the cheapest, so a block only patches when patching is cheaper than widening.
34//! This was measured before and rejected, and what changed is the numbering
35//! rather than the code: under a degree ordering the gaps in one block are all
36//! about the same size and there is nothing to patch, and under the community
37//! numbering in [`bisect`](crate::bisect) a block is mostly ones with a jump to
38//! another community in it, which is exactly the shape patching is for. It is
39//! worth 0.65 bits an edge on R-MAT, 2.36 on soc-LiveJournal1 as its ids arrive,
40//! and 5.41 on a bisected web-Google. On a uniform graph it is a wash, 15.98
41//! before and 15.96 after, because there the widths in a block already agree and
42//! the few blocks that do patch save about what the headers cost. It is worth
43//! about a tenth of the decode, which is the trade being made.
44//!
45//! Elias gamma, which needs no width at all, was 1.7 bits an edge worse on R-MAT
46//! and 7.7 worse on a uniform graph, and is not here.
47//!
48//! # What it costs
49//!
50//! Two things, and they pull in opposite directions. The payload is the gaps,
51//! and how small they are is a property of the graph rather than of the
52//! encoder: a uniformly random graph on `n` nodes with `m` edges cannot be
53//! stored below about `log2(n * n / m) + 1.44` bits an edge no matter what
54//! anybody does. The `8` bits an edge in `11` is not a claim about random
55//! graphs. It is paid for entirely by real graphs having hubs and communities,
56//! so neighbours cluster and the gaps between them are small.
57//!
58//! The overhead is the node offset table plus the per run header, and it is per
59//! node rather than per edge, so it is loud at degree one and inaudible at
60//! degree a hundred.
61//!
62//! The test at the bottom of this file measures both a uniformly random graph
63//! and an R-MAT graph, which is the standard synthetic social graph and the one
64//! Graph500 uses. At 65536 nodes and an average degree of 16:
65//!
66//! ```text
67//! total table head gaps
68//! uniform 15.96 1.06 1.31 13.58
69//! R-MAT 11.98 1.00 1.14 9.83
70//! R-MAT, degree ordered 9.38 0.69 0.78 7.90
71//! ```
72//!
73//! The uniform row is 15.96 against a floor of 13.44, so the encoder is within
74//! a fifth of what is provably possible on the case where nothing can help. The
75//! whole of the rest is the graph: R-MAT is 3.98 bits an edge cheaper for no
76//! other reason than that it has hubs, and [`order_by_degree`] takes another
77//! 2.60 off by giving those hubs the small ids. The same ordering pass moves a
78//! uniform graph by nothing at all, which is the control that says it is the
79//! structure being exploited rather than the measurement.
80//!
81//! # What it costs on a graph nobody here generated
82//!
83//! R-MAT is a stand in and it is known to cluster less than the social graphs it
84//! stands in for, so a real one should come out under 9.38. It does not.
85//! soc-LiveJournal1 from SNAP, 4847571 nodes and 68993773 edges, on server3,
86//! through `examples/compress.rs`:
87//!
88//! ```text
89//! total offsets degrees firsts widths gaps patches
90//! cold 17.99 1.18 0.49 1.43 0.82 10.58 3.47
91//! cold, degree ordered 19.00 1.13 0.27 1.43 0.74 14.57 0.84
92//! cold, bisected 15.00 1.14 0.42 1.44 0.91 7.53 3.55
93//! ```
94//!
95//! Three things in that table are worth saying out loud.
96//!
97//! The best number is 15.00 and it takes the numbering to get there. Before the
98//! patches and [`bisect`](crate::bisect) the same three rows were 20.35, 19.62
99//! and nothing, so this graph is a quarter smaller than it was and the whole of
100//! the difference is community structure that was there all along.
101//!
102//! Degree ordering now makes this graph bigger, 19.00 against 17.99. SNAP's ids
103//! are roughly the order the crawl found the accounts in, which already puts
104//! friends near each other, and sorting by degree throws that away. It was still
105//! the right call when the code could not exploit locality, and it stopped being
106//! the right call the moment the code could.
107//!
108//! The gaps are no longer most of the file. Under bisection the per node fields
109//! are 3.00 of the 15.00 and the payload is 11.99, against a floor, quoted by
110//! `--codes`, of 10.46 bits a gap or about 9.81 an edge. So the code is now 2.2
111//! bits over what any code that prices each gap on its own could do, where under
112//! degree ordering it was 1.18 over a floor that was itself six bits worse.
113//!
114//! ```text
115//! gaps 64685321, 24.9% of them 1, 16.6% of neighbours in a run of 4 or more
116//! by length floor 10.46 bits a gap
117//! block of 32 17.06 (unpatched, which is what this used to be)
118//! block of 8 13.99
119//! block of 32, patched 14.06 (the model, and the real one does better)
120//! elias delta 12.14
121//! intervals, block 8 13.78
122//! ```
123//!
124//! The 8 bits an edge in `11` is still not met and this is 15.00. What would
125//! close it is not another block code: the three that are left are all within a
126//! bit or two of each other and of the floor. It is a better numbering, and the
127//! measurement that says so is that bisection's own objective, a plain log gap
128//! code, prices this ordering at 9.29 bits a gap where it prices the degree
129//! ordering at 14.87. The numbering has found structure the encoder is only half
130//! spending. Interval encoding, which was dead on arrival under a degree
131//! ordering because only 0.9 percent of neighbours were consecutive, is now
132//! looking at 16.6 percent and is the first thing to try.
133//!
134//! # What this does not do
135//!
136//! It does not change. There is no link and no unlink, because every edge after
137//! the first in a run is written against the one before it, so touching one is
138//! rewriting the group. New edges go into the hot form and a later sweep folds
139//! them in, which is the promotion in `11` section 2 and is why O-G2, when the
140//! sweep should run, is still open.
141//!
142//! It does not renumber behind the caller's back. [`order_by_degree`] hands
143//! back a numbering and [`renumber`] applies it, and the caller keeps it,
144//! because the mapping from a caller's node id to a dense one is the node
145//! table's and there is no way to read the encoded graph without it.
146//!
147//! It does not hold the edge slots. A traversal wants to know where it can go
148//! next and nothing else, and the payload for that answer is what is packed
149//! here. Edge records are found by their own ids, which is the next piece.
150
151use crate::{Adjacency, Dir};
152
153/// Nodes to a group.
154///
155/// Five hundred and twelve, which is where the two costs cross. The offset
156/// table costs the log of the group's stream length per node, so halving the
157/// group saves about a bit a node; the group record costs sixteen bytes however
158/// many nodes are in it, so halving the group costs about one and a half bits a
159/// node. Anywhere from 256 to 1024 is within a few hundredths of a bit an edge
160/// of the best, and 512 is the middle of that.
161pub const GROUP: u32 = 512;
162
163/// Gaps to a block, each block with its own width.
164pub const BLOCK: usize = 32;
165
166/// One group's worth of what has to be known before its stream can be read.
167///
168/// Sixteen bytes, one per five hundred and twelve nodes, so a quarter of a bit
169/// a node and not worth thinking about again.
170#[derive(Debug, Clone, Copy, Default)]
171struct Group {
172 /// Bit offset of this group's node offset table in the word array.
173 at: u64,
174 /// Width of one entry in the node offset table.
175 ow: u8,
176 /// Width of a degree.
177 dw: u8,
178 /// The smallest neighbour id anything in this group points at. Every first
179 /// neighbour is written against this, which is most of what makes a
180 /// clustered graph cheaper than a random one.
181 ///
182 /// Writing it against the node's own id instead, which under a community
183 /// numbering is the better guess, was measured and is worse: the width is a
184 /// group wide maximum, one node in five hundred and twelve points a long
185 /// way off, and the sign bit is paid by all of them. It cost 0.11 bits an
186 /// edge on web-Google under every ordering.
187 base: u32,
188 /// Width of a first neighbour, once `base` is taken off it.
189 nw: u8,
190}
191
192/// A read only adjacency, node grouped and bit packed.
193///
194/// Built from an edge list over dense `u32` ids, or from the hot form through
195/// an id mapping. See the module documentation for the layout and for what it
196/// costs.
197///
198/// ```
199/// use yo_graph::Csr;
200///
201/// let mut edges = vec![(0u32, 3u32), (0, 1), (2, 0), (0, 9)];
202/// let cold = Csr::build(10, &mut edges);
203///
204/// assert_eq!(cold.degree(0), 3);
205/// assert_eq!(cold.neighbours(0), vec![1, 3, 9]);
206/// assert_eq!(cold.neighbours(1), Vec::<u32>::new());
207/// ```
208#[derive(Debug, Default)]
209pub struct Csr {
210 nodes: u32,
211 edges: u64,
212 groups: Vec<Group>,
213 words: Vec<u64>,
214 cost: Cost,
215}
216
217/// Where the bits went, in bits.
218///
219/// A compressed structure that cannot say which part of itself is expensive is
220/// very hard to improve, and the answer moves a lot between graphs: on a graph
221/// with an average degree of sixteen the per node fields are a fifth of the
222/// total, and on one with an average degree of three they are most of it.
223/// [`Csr::cost`] returns this and the `compress` example prints it.
224#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
225pub struct Cost {
226 /// The per node bit offset tables.
227 pub offsets: u64,
228 /// One degree per node, including the nodes that have no edges.
229 pub degrees: u64,
230 /// One first neighbour per node that has any.
231 pub firsts: u64,
232 /// The header in front of every block of gaps: the width, whether anything
233 /// did not fit it, and how much of it did not.
234 pub widths: u64,
235 /// The gaps, which is the only part that is really the graph.
236 pub gaps: u64,
237 /// The gaps that did not fit their block's width, written again at the end
238 /// of the block with the position they belong at.
239 pub patches: u64,
240 /// The fixed group records, which are not in the bit stream at all.
241 pub groups: u64,
242 /// Whatever rounding the stream up to whole words left over.
243 pub slack: u64,
244}
245
246impl Cost {
247 /// Everything, which is [`Csr::bytes`] in bits and to the bit.
248 #[must_use]
249 pub fn total(&self) -> u64 {
250 self.offsets
251 + self.degrees
252 + self.firsts
253 + self.widths
254 + self.gaps
255 + self.patches
256 + self.groups
257 + self.slack
258 }
259}
260
261impl Csr {
262 /// Encode an edge list.
263 ///
264 /// The list is sorted in place, which is the only reason it is taken by
265 /// mutable reference. Every id has to be under `nodes`. Parallel edges are
266 /// kept rather than merged, because whether two links between the same pair
267 /// are one edge or two is the caller's question and the answer costs a zero
268 /// gap either way.
269 #[must_use]
270 pub fn build(nodes: u32, edges: &mut [(u32, u32)]) -> Csr {
271 assert!(
272 edges.iter().all(|(s, d)| *s < nodes && *d < nodes),
273 "an edge names a node outside the graph"
274 );
275 edges.sort_unstable();
276 Csr::encode(nodes, edges)
277 }
278
279 /// Encode one label and direction of a hot plane.
280 ///
281 /// `id` maps the caller's node ids onto the dense `u32` ids the cold form
282 /// is over. That mapping is the node table's job and the node table does
283 /// not exist yet, so for now it is the caller's, which also means a caller
284 /// whose ids are already dense can pass a cast and pay nothing.
285 #[must_use]
286 pub fn from_hot(
287 hot: &Adjacency,
288 label: u32,
289 dir: Dir,
290 nodes: u32,
291 id: impl Fn(u64) -> u32,
292 ) -> Csr {
293 let mut edges = Vec::with_capacity(hot.edges());
294 hot.for_each_run(label, dir, |node, ns, _| {
295 let src = id(node);
296 edges.extend(ns.iter().map(|n| (src, id(*n))));
297 });
298 Csr::build(nodes, &mut edges)
299 }
300
301 /// How many node ids this was built over, whether or not they have edges.
302 #[must_use]
303 pub fn nodes(&self) -> u32 {
304 self.nodes
305 }
306
307 /// How many edges are packed in here.
308 #[must_use]
309 pub fn edges(&self) -> u64 {
310 self.edges
311 }
312
313 /// Whether there is nothing here.
314 #[must_use]
315 pub fn is_empty(&self) -> bool {
316 self.edges == 0
317 }
318
319 /// The degree of `node`, without decoding its run.
320 ///
321 /// Two dependent loads, the offset and then the degree that starts the run,
322 /// and neither of them touches a gap.
323 #[must_use]
324 pub fn degree(&self, node: u32) -> u32 {
325 let Some((g, i, count)) = self.locate(node) else {
326 return 0;
327 };
328 let s = self.groups[g];
329 let run = self.run_at(s, i, count);
330 read(&self.words, run, s.dw.into()) as u32
331 }
332
333 /// Decode the neighbours of `node` into `out`, ascending, replacing
334 /// whatever was in it.
335 ///
336 /// The buffer is the point: a walk decodes run after run and there is no
337 /// reason for any of them but the first to allocate.
338 pub fn neighbours_into(&self, node: u32, out: &mut Vec<u32>) {
339 out.clear();
340 let Some((g, i, count)) = self.locate(node) else {
341 return;
342 };
343 let s = self.groups[g];
344 let mut at = self.run_at(s, i, count);
345 let deg = read(&self.words, at, s.dw.into()) as usize;
346 at += u64::from(s.dw);
347 if deg == 0 {
348 return;
349 }
350 out.reserve(deg);
351 let mut cur = s.base + read(&self.words, at, s.nw.into()) as u32;
352 at += u64::from(s.nw);
353 out.push(cur);
354 let mut left = deg - 1;
355 // The block is read into here first, because a gap that was patched is
356 // only whole once its patch has been put back and the running sum cannot
357 // start until it is.
358 let mut block = [0u32; BLOCK];
359 while left > 0 {
360 let n = left.min(BLOCK);
361 let w = read(&self.words, at, 6) as u32;
362 at += 6;
363 let patched = read(&self.words, at, 1) == 1;
364 at += 1;
365 let (mut x, mut ew) = (0u64, 0u32);
366 if patched {
367 x = read(&self.words, at, POS) + 1;
368 at += u64::from(POS);
369 ew = read(&self.words, at, 6) as u32;
370 at += 6;
371 }
372 for slot in &mut block[..n] {
373 *slot = read(&self.words, at, w) as u32;
374 at += u64::from(w);
375 }
376 for _ in 0..x {
377 let pos = read(&self.words, at, POS) as usize;
378 at += u64::from(POS);
379 let high = read(&self.words, at, ew);
380 at += u64::from(ew);
381 block[pos] |= (high << w) as u32;
382 }
383 for gap in &block[..n] {
384 cur += gap;
385 out.push(cur);
386 }
387 left -= n;
388 }
389 }
390
391 /// The neighbours of `node`, ascending, in a fresh vector.
392 ///
393 /// The convenient one. A traversal should use
394 /// [`neighbours_into`](Csr::neighbours_into) and keep its buffer.
395 #[must_use]
396 pub fn neighbours(&self, node: u32) -> Vec<u32> {
397 let mut out = Vec::new();
398 self.neighbours_into(node, &mut out);
399 out
400 }
401
402 /// Ask the cache for the word a node's offset will be found in.
403 ///
404 /// Same reason as the hot form's: a frontier is known before any of it is
405 /// read, and the loads that decode it are dependent, so the only way to
406 /// make them cheap is to stop them being serial.
407 pub fn prefetch(&self, node: u32) {
408 let Some((g, i, _)) = self.locate(node) else {
409 return;
410 };
411 let s = self.groups[g];
412 let bit = s.at + i * u64::from(s.ow);
413 yo_common::prefetch(&self.words[(bit / 64) as usize]);
414 }
415
416 /// Resident bytes, everything included.
417 #[must_use]
418 pub fn bytes(&self) -> usize {
419 self.words.capacity() * size_of::<u64>() + self.groups.capacity() * size_of::<Group>()
420 }
421
422 /// Where the bits went. See [`Cost`].
423 #[must_use]
424 pub fn cost(&self) -> Cost {
425 self.cost
426 }
427
428 /// [`bytes`](Csr::bytes) said the way the target in `11` is written, and
429 /// zero for a graph with no edges.
430 #[must_use]
431 pub fn bits_per_edge(&self) -> f64 {
432 if self.edges == 0 {
433 return 0.0;
434 }
435 self.bytes() as f64 * 8.0 / self.edges as f64
436 }
437
438 /// The group a node is in, its index within it, and how many nodes that
439 /// group holds, which is fewer than [`GROUP`] for the last one.
440 #[inline]
441 fn locate(&self, node: u32) -> Option<(usize, u64, u64)> {
442 if node >= self.nodes {
443 return None;
444 }
445 let g = node / GROUP;
446 let lo = g * GROUP;
447 Some((
448 g as usize,
449 u64::from(node - lo),
450 u64::from((lo + GROUP).min(self.nodes) - lo),
451 ))
452 }
453
454 /// Where a node's run starts, which is one read of the offset table.
455 #[inline]
456 fn run_at(&self, s: Group, i: u64, count: u64) -> u64 {
457 let table = s.at + i * u64::from(s.ow);
458 s.at + count * u64::from(s.ow) + read(&self.words, table, s.ow.into())
459 }
460
461 /// The whole encoder, over an edge list already sorted by source and then
462 /// by destination.
463 fn encode(nodes: u32, edges: &[(u32, u32)]) -> Csr {
464 let mut w = Writer::default();
465 let mut groups = Vec::with_capacity(nodes.div_ceil(GROUP) as usize);
466 let mut runs: Vec<(usize, usize)> = Vec::with_capacity(GROUP as usize);
467 let mut offs: Vec<u64> = Vec::with_capacity(GROUP as usize);
468 let mut gaps: Vec<u32> = Vec::new();
469 let mut e = 0usize;
470 let mut cost = Cost::default();
471
472 for lo in (0..nodes).step_by(GROUP as usize) {
473 let hi = (lo + GROUP).min(nodes);
474 let count = (hi - lo) as usize;
475
476 // Cut the group's edges into one run per node and learn the three
477 // widths the group needs before anything is written.
478 runs.clear();
479 let (mut base, mut top, mut maxdeg) = (u32::MAX, 0u32, 0u32);
480 for node in lo..hi {
481 let s = e;
482 while e < edges.len() && edges[e].0 == node {
483 e += 1;
484 }
485 runs.push((s, e));
486 maxdeg = maxdeg.max((e - s) as u32);
487 if e > s {
488 base = base.min(edges[s].1);
489 top = top.max(edges[e - 1].1);
490 }
491 }
492 let base = if base == u32::MAX { 0 } else { base };
493 let dw = width(u64::from(maxdeg));
494 let nw = width(u64::from(top.saturating_sub(base)));
495
496 // Then how long every run is, because the offset table is written
497 // before the runs are and its own width comes from their total.
498 offs.clear();
499 let mut total = 0u64;
500 for (s, t) in &runs {
501 offs.push(total);
502 gaps_of(&edges[*s..*t], &mut gaps);
503 total += run_bits(&gaps, *t > *s, dw, nw);
504 }
505 let ow = width(total);
506
507 let at = w.bits();
508 w.skip(count as u64 * u64::from(ow));
509 cost.offsets += count as u64 * u64::from(ow);
510 for (i, (s, t)) in runs.iter().enumerate() {
511 w.put_at(at + i as u64 * u64::from(ow), offs[i], ow);
512 let run = &edges[*s..*t];
513 w.put(run.len() as u64, dw);
514 cost.degrees += u64::from(dw);
515 if run.is_empty() {
516 continue;
517 }
518 w.put(u64::from(run[0].1 - base), nw);
519 cost.firsts += u64::from(nw);
520 gaps_of(run, &mut gaps);
521 for block in gaps.chunks(BLOCK) {
522 let p = Plan::best(block);
523 w.put(u64::from(p.w), 6);
524 w.put(u64::from(p.x != 0), 1);
525 cost.widths += 7;
526 if p.x != 0 {
527 w.put(u64::from(p.x - 1), POS);
528 w.put(u64::from(p.ew), 6);
529 cost.widths += 11;
530 }
531 cost.gaps += block.len() as u64 * u64::from(p.w);
532 for gap in block {
533 w.put(u64::from(*gap) & mask(p.w), p.w);
534 }
535 // The part of a gap that did not fit, at its position in the
536 // block, which the reader puts back on top of what it read.
537 for (at, gap) in block.iter().enumerate() {
538 if u64::from(*gap) >> p.w != 0 {
539 w.put(at as u64, POS);
540 w.put(u64::from(*gap) >> p.w, p.ew);
541 cost.patches += u64::from(POS + p.ew);
542 }
543 }
544 }
545 }
546 groups.push(Group {
547 at,
548 base,
549 ow: ow as u8,
550 dw: dw as u8,
551 nw: nw as u8,
552 });
553 }
554
555 // One spare word, so a field that ends flush against the last one can
556 // still be read by the two word path without a bounds check for it.
557 w.words.push(0);
558 w.words.shrink_to_fit();
559 groups.shrink_to_fit();
560 // The spare word and the rounding, so the total is the resident size to
561 // the bit rather than to the field. Taken before the group records go
562 // in, because those are not in the stream.
563 cost.slack = w.words.capacity() as u64 * 64 - cost.total();
564 cost.groups = groups.capacity() as u64 * size_of::<Group>() as u64 * 8;
565 Csr {
566 nodes,
567 edges: edges.len() as u64,
568 groups,
569 words: w.words,
570 cost,
571 }
572 }
573}
574
575/// The distances between one run's neighbours, into a buffer the encoder keeps
576/// across runs so a hub does not allocate.
577fn gaps_of(run: &[(u32, u32)], into: &mut Vec<u32>) {
578 into.clear();
579 into.extend(run.windows(2).map(|p| p[1].1 - p[0].1));
580}
581
582/// A node numbering that makes the graph smaller, busiest node first.
583///
584/// Returns the new id of every old id, so `out[old]` is `new`. Apply it with
585/// [`renumber`] and encode the result.
586///
587/// This is the cheapest ordering there is, one count and one sort, and on a
588/// power law graph it is worth about a fifth of the whole size. The reason is
589/// that in a graph with hubs, almost every neighbour list contains some of the
590/// hubs, and giving the hubs the smallest ids puts that shared part of every
591/// list at the front where the gaps between its members are single digits. On
592/// the R-MAT graph in the test below it takes 11.98 bits an edge to 9.38.
593///
594/// The control matters as much as the result. On a uniformly random graph the
595/// same pass changes nothing at all, 15.96 bits an edge before and after, which
596/// is what says it is exploiting structure rather than being an artefact of the
597/// encoder. Breadth first from the busiest node, taking each frontier in degree
598/// order, was measured against this and came out slightly worse at 10.15, so it
599/// is not here.
600///
601/// This is the cheap ordering and it is not the good one. On a real graph the
602/// numbering that pays is [`bisect::order`](crate::bisect::order), which finds
603/// communities rather than hubs and is minutes rather than one sort. It beats
604/// this by 5.2 bits an edge on web-Google. On R-MAT it does not beat this at
605/// all, because R-MAT's structure is its hubs and this pass is already the right
606/// answer for those.
607#[must_use]
608pub fn order_by_degree(nodes: u32, edges: &[(u32, u32)]) -> Vec<u32> {
609 let mut deg = vec![0u32; nodes as usize];
610 for (s, d) in edges {
611 deg[*s as usize] += 1;
612 deg[*d as usize] += 1;
613 }
614 let mut order: Vec<u32> = (0..nodes).collect();
615 // Ties by the old id, so the same graph always numbers the same way.
616 order.sort_unstable_by_key(|n| (core::cmp::Reverse(deg[*n as usize]), *n));
617 let mut to = vec![0u32; nodes as usize];
618 for (new, old) in order.iter().enumerate() {
619 to[*old as usize] = new as u32;
620 }
621 to
622}
623
624/// Rewrite an edge list under a numbering, in place.
625pub fn renumber(edges: &mut [(u32, u32)], to: &[u32]) {
626 for e in edges {
627 *e = (to[e.0 as usize], to[e.1 as usize]);
628 }
629}
630
631/// How one block of gaps goes out: a width every gap in it is written at, and
632/// the ones that did not fit written again at the end.
633///
634/// A block of thirty two gaps under a community numbering is mostly ones with
635/// the occasional jump to another community in it, and a width that has to hold
636/// the jump charges all thirty two of them for it. Leaving the jump behind as a
637/// patch is what stops that, and it is only worth doing because the numbering
638/// makes the distribution that shape. Under a degree ordering the same thing
639/// costs 0.08 bits an edge, which is why it was not here before.
640#[derive(Debug, Clone, Copy, Default)]
641struct Plan {
642 /// The width every gap in the block is written at.
643 w: u32,
644 /// How many of them did not fit.
645 x: u32,
646 /// The width the part that did not fit is written at, which is the widest
647 /// gap in the block less `w`.
648 ew: u32,
649}
650
651/// Bits of position in a patch, which is what indexes a block.
652const POS: u32 = 5;
653
654impl Plan {
655 /// The cheapest way to write one block.
656 ///
657 /// Walking the candidate width down from the widest gap, `over` is how many
658 /// gaps do not fit it. The widest gap is an exception at every width below
659 /// its own, so the width the patches need is always the widest less the
660 /// candidate, and the whole search is a walk over a histogram rather than a
661 /// pass over the block per candidate.
662 fn best(block: &[u32]) -> Plan {
663 let mut hist = [0u32; 33];
664 let mut top = 0u32;
665 for g in block {
666 let b = width(u64::from(*g));
667 hist[b as usize] += 1;
668 top = top.max(b);
669 }
670 let n = block.len() as u64;
671 let mut best = Plan {
672 w: top,
673 x: 0,
674 ew: 0,
675 };
676 let mut cost = 7 + n * u64::from(top);
677 let mut over = 0u32;
678 for w in (0..top).rev() {
679 over += hist[w as usize + 1];
680 let plan = Plan {
681 w,
682 x: over,
683 ew: top - w,
684 };
685 let bits = plan.bits(n);
686 if bits < cost {
687 (best, cost) = (plan, bits);
688 }
689 }
690 best
691 }
692
693 /// What this block costs, header and payload and patches.
694 fn bits(&self, n: u64) -> u64 {
695 let head = if self.x == 0 { 7 } else { 7 + 11 };
696 head + n * u64::from(self.w) + u64::from(self.x) * u64::from(POS + self.ew)
697 }
698}
699
700/// How many bits one run takes, which has to agree exactly with what the
701/// encoder then writes.
702fn run_bits(gaps: &[u32], any: bool, dw: u32, nw: u32) -> u64 {
703 if !any {
704 return u64::from(dw);
705 }
706 let mut bits = u64::from(dw) + u64::from(nw);
707 for block in gaps.chunks(BLOCK) {
708 bits += Plan::best(block).bits(block.len() as u64);
709 }
710 bits
711}
712
713/// How many bits it takes to hold `v`, and none for zero.
714#[inline]
715fn width(v: u64) -> u32 {
716 64 - v.leading_zeros()
717}
718
719#[inline]
720fn mask(w: u32) -> u64 {
721 if w == 64 { u64::MAX } else { (1u64 << w) - 1 }
722}
723
724/// `w` bits starting at bit `at`, out of at most two words.
725///
726/// A field only ever crosses a word boundary when it did not start on one, so
727/// the shift in the second branch is never sixty four.
728#[inline]
729fn read(words: &[u64], at: u64, w: u32) -> u64 {
730 if w == 0 {
731 return 0;
732 }
733 let i = (at / 64) as usize;
734 let off = (at % 64) as u32;
735 let lo = words[i] >> off;
736 let got = 64 - off;
737 if got >= w {
738 lo & mask(w)
739 } else {
740 (lo | (words[i + 1] << got)) & mask(w)
741 }
742}
743
744/// A bit stream being written, forwards, with the ability to go back and fill
745/// in a hole it left on purpose.
746#[derive(Debug, Default)]
747struct Writer {
748 words: Vec<u64>,
749 bits: u64,
750}
751
752impl Writer {
753 #[inline]
754 fn bits(&self) -> u64 {
755 self.bits
756 }
757
758 /// Leave `n` bits alone, for something that is not known yet. They are
759 /// zero, which is what [`Writer::put_at`] needs them to be.
760 fn skip(&mut self, n: u64) {
761 self.bits += n;
762 self.room(self.bits);
763 }
764
765 fn put(&mut self, v: u64, w: u32) {
766 self.put_at(self.bits, v, w);
767 self.bits += u64::from(w);
768 }
769
770 /// Write into bits that are still zero, anywhere in the stream.
771 fn put_at(&mut self, at: u64, v: u64, w: u32) {
772 if w == 0 {
773 return;
774 }
775 self.room(at + u64::from(w));
776 let i = (at / 64) as usize;
777 let off = (at % 64) as u32;
778 let v = v & mask(w);
779 debug_assert!(w == 64 || v >> w == 0, "a field wider than it was given");
780 self.words[i] |= v << off;
781 if off + w > 64 {
782 self.words[i + 1] |= v >> (64 - off);
783 }
784 }
785
786 fn room(&mut self, upto: u64) {
787 let need = (upto as usize).div_ceil(64) + 1;
788 if self.words.len() < need {
789 self.words.resize(need, 0);
790 }
791 }
792}
793
794#[cfg(test)]
795mod tests {
796 use super::*;
797 use yo_common::Rng;
798
799 /// The reference every round trip test is checked against: what the edge
800 /// list plainly says, sorted, one vector a node.
801 fn reference(nodes: u32, edges: &[(u32, u32)]) -> Vec<Vec<u32>> {
802 let mut out = vec![Vec::new(); nodes as usize];
803 for (s, d) in edges {
804 out[*s as usize].push(*d);
805 }
806 for v in &mut out {
807 v.sort_unstable();
808 }
809 out
810 }
811
812 fn agrees(nodes: u32, mut edges: Vec<(u32, u32)>) -> Csr {
813 let want = reference(nodes, &edges);
814 let cold = Csr::build(nodes, &mut edges);
815 let mut got = Vec::new();
816 for node in 0..nodes {
817 cold.neighbours_into(node, &mut got);
818 assert_eq!(got, want[node as usize], "node {node}");
819 assert_eq!(
820 cold.degree(node),
821 want[node as usize].len() as u32,
822 "degree of {node}"
823 );
824 }
825 cold
826 }
827
828 /// A uniformly random graph, which is the case no encoder can win.
829 fn uniform(nodes: u32, degree: u32, seed: u64) -> Vec<(u32, u32)> {
830 let mut rng = Rng::new(seed);
831 let mut edges = Vec::with_capacity((nodes as usize) * (degree as usize));
832 for src in 0..nodes {
833 for _ in 0..degree {
834 edges.push((src, (rng.next_u64() % u64::from(nodes)) as u32));
835 }
836 }
837 edges
838 }
839
840 /// R-MAT with the Graph500 probabilities, which is the standard synthetic
841 /// stand in for a social graph and the case where community structure is
842 /// what pays for the compression.
843 fn rmat(scale: u32, degree: u32, seed: u64) -> Vec<(u32, u32)> {
844 let nodes = 1u32 << scale;
845 let mut rng = Rng::new(seed);
846 let mut edges = Vec::with_capacity((nodes as usize) * (degree as usize));
847 for _ in 0..(nodes as u64) * u64::from(degree) {
848 let (mut r, mut c) = (0u32, 0u32);
849 for level in 0..scale {
850 let bit = 1u32 << (scale - 1 - level);
851 let p = (rng.next_u64() >> 11) as f64 / (1u64 << 53) as f64;
852 if p < 0.57 {
853 } else if p < 0.76 {
854 c |= bit;
855 } else if p < 0.95 {
856 r |= bit;
857 } else {
858 r |= bit;
859 c |= bit;
860 }
861 }
862 edges.push((r, c));
863 }
864 edges
865 }
866
867 #[test]
868 fn a_field_comes_back_out_the_way_it_went_in() {
869 let mut w = Writer::default();
870 let mut rng = Rng::new(7);
871 let mut wrote = Vec::new();
872 for _ in 0..5000 {
873 let bits = (rng.next_u64() % 33) as u32;
874 let v = rng.next_u64() & mask(bits);
875 wrote.push((w.bits(), v, bits));
876 w.put(v, bits);
877 }
878 w.words.push(0);
879 for (at, v, bits) in wrote {
880 assert_eq!(read(&w.words, at, bits), v, "at {at} wide {bits}");
881 }
882 }
883
884 #[test]
885 fn a_hole_left_on_purpose_can_be_filled_in_later() {
886 let mut w = Writer::default();
887 let at = w.bits();
888 w.skip(40);
889 w.put(0xabcd, 32);
890 w.put_at(at, 0x9f_ffff_ffff, 40);
891 w.words.push(0);
892 assert_eq!(read(&w.words, at, 40), 0x9f_ffff_ffff);
893 assert_eq!(read(&w.words, at + 40, 32), 0xabcd);
894 }
895
896 #[test]
897 fn the_bits_add_up_to_the_bytes() {
898 let mut edges = rmat(12, 8, 0xadd);
899 let cold = Csr::build(1 << 12, &mut edges);
900 let c = cold.cost();
901 assert_eq!(c.total(), cold.bytes() as u64 * 8, "{c:?}");
902 assert!(
903 c.gaps > c.offsets,
904 "the gaps should be the biggest part here"
905 );
906 }
907
908 #[test]
909 fn an_empty_graph_is_a_graph() {
910 let cold = Csr::build(0, &mut []);
911 assert!(cold.is_empty());
912 assert_eq!(cold.degree(0), 0);
913 assert_eq!(cold.neighbours(0), Vec::<u32>::new());
914 assert_eq!(cold.bits_per_edge(), 0.0);
915 }
916
917 #[test]
918 fn nodes_with_no_edges_are_still_nodes() {
919 let cold = agrees(1000, vec![(500, 1), (500, 2)]);
920 assert_eq!(cold.nodes(), 1000);
921 assert_eq!(cold.edges(), 2);
922 assert_eq!(
923 cold.degree(1000),
924 0,
925 "and past the end is nothing rather than a panic"
926 );
927 }
928
929 #[test]
930 fn a_run_comes_back_ascending_however_it_went_in() {
931 agrees(64, vec![(3, 40), (3, 1), (3, 63), (3, 0), (3, 17)]);
932 }
933
934 #[test]
935 fn parallel_edges_survive_as_the_zero_gaps_they_are() {
936 let cold = agrees(16, vec![(1, 2), (1, 2), (1, 2), (1, 9)]);
937 assert_eq!(cold.degree(1), 4);
938 assert_eq!(cold.neighbours(1), vec![2, 2, 2, 9]);
939 }
940
941 #[test]
942 fn a_self_loop_is_an_edge_like_any_other() {
943 agrees(8, vec![(4, 4), (4, 0)]);
944 }
945
946 #[test]
947 fn the_last_group_can_be_a_partial_one() {
948 // Deliberately not a multiple of the group size, and with edges in the
949 // stub group, because an off by one there reads another group's table.
950 let nodes = GROUP * 2 + 5;
951 let mut edges = Vec::new();
952 for src in 0..nodes {
953 edges.push((src, (src * 7) % nodes));
954 }
955 agrees(nodes, edges);
956 }
957
958 #[test]
959 fn a_hub_spans_as_many_blocks_as_it_needs() {
960 // Two hundred thousand edges out of one node is over six thousand
961 // blocks, and one neighbour placed far away so the run is not uniform.
962 let mut edges: Vec<(u32, u32)> = (0..200_000u32).map(|i| (1, i)).collect();
963 edges.push((1, 999_999));
964 let cold = agrees(1_000_000, edges);
965 assert_eq!(cold.degree(1), 200_001);
966 }
967
968 #[test]
969 fn a_block_of_one_enormous_gap_does_not_price_the_rest() {
970 // The whole reason the width is per block. Fifty thousand edges one
971 // apart and a single jump across the graph. Per run widths would charge
972 // all fifty thousand of them twenty bits; per block widths charge
973 // thirty two of them.
974 let mut edges: Vec<(u32, u32)> = (0..50_000u32).map(|i| (7, i)).collect();
975 edges.push((7, 99_999));
976 let cold = Csr::build(100_000, &mut edges.clone());
977 agrees(100_000, edges);
978 assert!(
979 cold.bits_per_edge() < 4.0,
980 "one far neighbour priced the whole run at {:.2} bits an edge",
981 cold.bits_per_edge()
982 );
983 }
984
985 #[test]
986 fn the_cold_form_agrees_with_a_graph_someone_made_up() {
987 let mut rng = Rng::new(0x51de);
988 let nodes = 5000u32;
989 let mut edges = Vec::new();
990 for _ in 0..60_000 {
991 // A degree distribution with a tail, so groups differ in every
992 // width they choose and the partial and empty runs are both hit.
993 let src = if rng.next_u64().is_multiple_of(10) {
994 (rng.next_u64() % 20) as u32
995 } else {
996 (rng.next_u64() % u64::from(nodes)) as u32
997 };
998 edges.push((src, (rng.next_u64() % u64::from(nodes)) as u32));
999 }
1000 agrees(nodes, edges);
1001 }
1002
1003 #[test]
1004 fn promotion_reads_what_the_hot_plane_holds() {
1005 const FOLLOWS: u32 = 1;
1006 const BLOCKS: u32 = 2;
1007 let mut hot = Adjacency::new();
1008 let mut rng = Rng::new(0x40ce);
1009 let mut want: Vec<Vec<u32>> = vec![Vec::new(); 4000];
1010 for _ in 0..40_000 {
1011 let (s, d) = (rng.next_u64() % 4000, rng.next_u64() % 4000);
1012 hot.link(s, d, FOLLOWS, 0);
1013 want[s as usize].push(d as u32);
1014 }
1015 // Another label, which promotion has to leave behind entirely.
1016 for _ in 0..1000 {
1017 hot.link(rng.next_u64() % 4000, rng.next_u64() % 4000, BLOCKS, 0);
1018 }
1019 for v in &mut want {
1020 v.sort_unstable();
1021 }
1022
1023 let cold = Csr::from_hot(&hot, FOLLOWS, Dir::Out, 4000, |n| n as u32);
1024 assert_eq!(cold.edges(), 40_000);
1025 let mut got = Vec::new();
1026 for node in 0..4000u32 {
1027 cold.neighbours_into(node, &mut got);
1028 assert_eq!(got, want[node as usize], "node {node}");
1029 }
1030
1031 // And the transpose, which the hot plane indexes and which has to come
1032 // out as the mirror of what went in.
1033 let mut mirror: Vec<Vec<u32>> = vec![Vec::new(); 4000];
1034 for (s, ds) in want.iter().enumerate() {
1035 for d in ds {
1036 mirror[*d as usize].push(s as u32);
1037 }
1038 }
1039 for v in &mut mirror {
1040 v.sort_unstable();
1041 }
1042 let back = Csr::from_hot(&hot, FOLLOWS, Dir::In, 4000, |n| n as u32);
1043 for node in 0..4000u32 {
1044 back.neighbours_into(node, &mut got);
1045 assert_eq!(got, mirror[node as usize], "incoming to {node}");
1046 }
1047 }
1048
1049 /// The number the target in `11` is about, on the two graphs that bracket
1050 /// it: one where nothing can help and one shaped like the graphs the target
1051 /// was written for.
1052 #[test]
1053 fn what_a_random_graph_costs_and_what_a_real_one_saves() {
1054 let nodes = 1u32 << 16;
1055 let degree = 16u32;
1056
1057 let mut random = uniform(nodes, degree, 0xbeef);
1058 let random = Csr::build(nodes, &mut random);
1059
1060 // The floor for a uniformly random graph, log2(n * n / m) + 1.44 bits
1061 // an edge, which here is about 13.4. Nothing beats it, so the only
1062 // question is how close the encoder gets.
1063 let floor =
1064 ((f64::from(nodes) * f64::from(nodes)) / f64::from(nodes * degree)).log2() + 1.44;
1065 let got = random.bits_per_edge();
1066 assert!(
1067 got > floor - 0.5,
1068 "random graph at {got:.2} bits an edge is under its {floor:.2} bit floor, so something is not being counted"
1069 );
1070 assert!(
1071 got < floor * 1.35,
1072 "random graph at {got:.2} bits an edge against a floor of {floor:.2}, so the encoder is wasting a third of itself"
1073 );
1074
1075 // The same shape of graph with hubs in it. Everything below the uniform
1076 // number is the structure rather than the encoder.
1077 let mut social = rmat(16, degree, 0xf00d);
1078 let flat = Csr::build(nodes, &mut social.clone()).bits_per_edge();
1079 assert!(
1080 flat < got - 2.5,
1081 "R-MAT at {flat:.2} bits an edge against uniform at {got:.2}, so having hubs is buying nothing"
1082 );
1083
1084 // And the ordering pass on top, which is the last cheap lever.
1085 let to = order_by_degree(nodes, &social);
1086 renumber(&mut social, &to);
1087 let ordered = Csr::build(nodes, &mut social).bits_per_edge();
1088 assert!(
1089 ordered < flat - 2.0,
1090 "degree ordering took R-MAT from {flat:.2} to {ordered:.2} bits an edge, which is not the fifth it was measured at"
1091 );
1092 assert!(
1093 ordered < 10.5,
1094 "R-MAT degree ordered at {ordered:.2} bits an edge, against the 9.89 this was measured at"
1095 );
1096 }
1097
1098 /// The control on the ordering pass. It has to be worth nothing at all on a
1099 /// graph with no structure in it, because if it moves this number then it
1100 /// is not doing what it says it is doing.
1101 #[test]
1102 fn ordering_a_graph_with_no_structure_saves_nothing() {
1103 let nodes = 1u32 << 16;
1104 let mut edges = uniform(nodes, 16, 0xbeef);
1105 let before = Csr::build(nodes, &mut edges.clone()).bits_per_edge();
1106 let to = order_by_degree(nodes, &edges);
1107 renumber(&mut edges, &to);
1108 let after = Csr::build(nodes, &mut edges).bits_per_edge();
1109 assert!(
1110 (after - before).abs() < 0.1,
1111 "degree ordering moved a uniform graph from {before:.2} to {after:.2} bits an edge"
1112 );
1113 }
1114
1115 #[test]
1116 fn a_numbering_is_a_permutation_and_the_graph_survives_it() {
1117 let mut edges = vec![(0u32, 1u32), (0, 2), (0, 3), (5, 0), (5, 1), (9, 0)];
1118 let to = order_by_degree(10, &edges);
1119 let mut seen = to.clone();
1120 seen.sort_unstable();
1121 assert_eq!(seen, (0..10).collect::<Vec<u32>>(), "not a permutation");
1122 assert_eq!(to[0], 0, "the busiest node did not get the smallest id");
1123
1124 // Every edge is still the same edge, just under other names.
1125 let before: Vec<(u32, u32)> = edges.clone();
1126 renumber(&mut edges, &to);
1127 let mapped: Vec<(u32, u32)> = before
1128 .iter()
1129 .map(|(s, d)| (to[*s as usize], to[*d as usize]))
1130 .collect();
1131 assert_eq!(edges, mapped);
1132
1133 let cold = Csr::build(10, &mut edges);
1134 assert_eq!(cold.edges(), 6);
1135 assert_eq!(cold.degree(to[0] as u32), 3);
1136 assert_eq!(cold.degree(to[5] as u32), 2);
1137 }
1138
1139 /// The cold form against the hot one, which is the whole reason it exists.
1140 #[test]
1141 fn the_cold_form_is_an_order_of_magnitude_under_the_hot_one() {
1142 let nodes = 1u32 << 16;
1143 let mut edges = rmat(16, 16, 0x0117);
1144 let mut hot = Adjacency::out_only();
1145 for (s, d) in &edges {
1146 hot.link(u64::from(*s), u64::from(*d), 1, 0);
1147 }
1148 hot.compact();
1149 let cold = Csr::build(nodes, &mut edges);
1150
1151 let ratio = hot.bytes() as f64 / cold.bytes() as f64;
1152 assert!(
1153 ratio > 8.0,
1154 "the cold form is only {ratio:.1} times smaller than the hot one, at {:.2} bits an edge against {:.1} bytes",
1155 cold.bits_per_edge(),
1156 hot.bytes() as f64 / hot.edges() as f64
1157 );
1158 }
1159}