Skip to main content

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}