yo_graph/adjacency.rs
1//! The adjacency plane in its hot form: a run of neighbours per (node, label,
2//! direction), appended to and deleted from in place.
3//!
4//! A graph without a query language is an adjacency structure with good
5//! ergonomics, so this is what everything else in the graph model stands on.
6//! `11` section 2 gives adjacency two forms and this is the mutable one. The
7//! other is zu's node group CSR, which reaches 8 bits an edge because it never
8//! changes; this one is 12 bytes an edge because every operation on it has to
9//! be O(1).
10//!
11//! # The shape
12//!
13//! A run is the neighbours of one node under one label in one direction, and it
14//! is contiguous. That is the whole performance argument: a one hop is a probe
15//! for the run header and then a sequential read, and the read is over `u64`
16//! node ids with nothing else interleaved, so eight neighbours arrive per cache
17//! line.
18//!
19//! The edge slots live in a second array indexed the same way. Keeping them
20//! apart rather than storing `(neighbour, edge)` pairs costs nothing and saves
21//! a third of the memory traffic on the common walk, because a traversal that
22//! only wants to know where it can go next never reads an edge slot at all.
23//! Interleaving them would also make every neighbour an unaligned load out of a
24//! 12 byte stride.
25//!
26//! # Growing and shrinking
27//!
28//! Runs are cut from two shared arenas rather than allocated one by one,
29//! because a graph is mostly nodes with a handful of edges and a `Vec` header
30//! per node would cost more than the edges do. A run's capacity comes off a
31//! fixed ladder: doubling while it is small, then a quarter more each step, so
32//! the slack a hub carries is bounded by 25 per cent instead of by 100. Growing
33//! copies the run into the next size up and gives the old block to a free list,
34//! so the space is reused rather than lost.
35//!
36//! Deleting is swap with last and a decrement, which is how `08` section 4
37//! deletes from a dense member vector and it is the same reason: an O(1) delete
38//! is worth giving up the order of a run that has no order to give up. A run
39//! that falls to half of its capacity is moved down to the smallest size that
40//! fits, which leaves 2x of hysteresis so a run sitting on a boundary does not
41//! copy itself every time it gains and loses one edge.
42//!
43//! # What it costs
44//!
45//! Twelve bytes an edge is the payload and it is not the whole bill. There is
46//! one 32 byte run header per (node, label, direction) that has ever been
47//! linked, and there is the capacity slack. On a graph shaped like LiveJournal,
48//! most nodes with a few edges and a thin tail of hubs, the measured numbers
49//! are 18.1 bytes an edge as it is built and 15.2 after a sweep, which is 12.0
50//! of payload and 3.2 of run headers against an average degree of 13. The test
51//! at the bottom of this file is where those come from.
52//!
53//! The cold form is where 8 bits an edge comes from, and the ladder in `03` is
54//! what lets both be true at once. Against what this replaces it is already the
55//! cheap end: a pointer chased adjacency list is 16 bytes for the pair before
56//! the per node allocation header, and Neo4j's relationship store is 34.
57//!
58//! # What this does not do
59//!
60//! It does not look for a duplicate before it links. That check is linear in
61//! the degree, and the degree is exactly the thing that can be a hub with
62//! twenty thousand edges on it, so paying it on every insert would trade the
63//! write path away to enforce something the layer above can enforce with one
64//! probe. An edge table keyed by (source, destination, label) is where upsert
65//! semantics belong, and it is what `G.EADD` will sit on.
66//!
67//! It also does not delete a node, because finding every run a node has means
68//! knowing every label it was ever linked under, and that is the node table's
69//! job rather than this one's.
70
71use yo_common::prefetch;
72
73/// Which end of an edge a run is stored under.
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
75pub enum Dir {
76 /// Edges leaving the node.
77 Out,
78 /// Edges arriving at the node.
79 In,
80}
81
82/// How many sizes the capacity ladder has.
83///
84/// Eighty eight of them reach a run of four billion edges, which is the most a
85/// `u32` offset can address anyway, and the rest are there so the arithmetic at
86/// the top never has to think about the end.
87const CLASSES: usize = 96;
88
89/// The capacity ladder, in edges.
90const LADDER: [u32; CLASSES] = ladder();
91
92const LIVE: u8 = 1;
93const INCOMING: u8 = 2;
94
95/// Doubling to 16 and then a quarter more, rounded up to four so the steps just
96/// past the change of policy are still steps.
97///
98/// Sixteen is where the doubling stops and it is measured rather than picked. A
99/// run of 30 edges in a run sized 32 wastes nothing worth counting, but a run of
100/// 33 in a run sized 64 wastes half of itself, and the degrees between about 5
101/// and 50 are where most of a real graph's edges live. Doubling all the way to
102/// 64 costs 1.47 bytes of capacity per byte of edge across that band; stopping
103/// at 16 costs 1.16, and the whole graph goes from 1.23 to 1.13. What it buys
104/// back is two more copies per edge over the life of a run, which is 24 bytes of
105/// memcpy against 12 bytes of edge, and that is not a trade anybody notices.
106const fn ladder() -> [u32; CLASSES] {
107 let mut out = [0u32; CLASSES];
108 let mut cap: u64 = 1;
109 let mut i = 0;
110 while i < CLASSES {
111 out[i] = if cap > u32::MAX as u64 {
112 u32::MAX
113 } else {
114 cap as u32
115 };
116 cap = if cap < 16 {
117 cap * 2
118 } else {
119 (cap + cap / 4 + 3) & !3
120 };
121 i += 1;
122 }
123 out
124}
125
126/// The header of one run, and the only thing the table stores.
127///
128/// Thirty two bytes, so two of them fit a cache line and neither straddles it,
129/// which is what makes a probe that misses cost one load and nothing else.
130#[derive(Debug, Clone, Copy, Default)]
131struct Slot {
132 node: u64,
133 at: u32,
134 len: u32,
135 cap: u32,
136 label: u32,
137 flags: u8,
138}
139
140/// The neighbours of every node, under every label, in both directions.
141///
142/// ```
143/// use yo_graph::{Adjacency, Dir};
144///
145/// const FOLLOWS: u32 = 1;
146///
147/// let mut g = Adjacency::new();
148/// g.link(1, 2, FOLLOWS, 100);
149/// g.link(1, 3, FOLLOWS, 101);
150///
151/// assert_eq!(g.neighbours(1, FOLLOWS, Dir::Out), &[2, 3]);
152/// assert_eq!(g.neighbours(2, FOLLOWS, Dir::In), &[1]);
153/// assert_eq!(g.degree(1, FOLLOWS, Dir::Out), 2);
154/// ```
155#[derive(Debug)]
156pub struct Adjacency {
157 slots: Vec<Slot>,
158 live: usize,
159 filled: usize,
160 edges: usize,
161 entries: usize,
162 neighbour: Vec<u64>,
163 edge: Vec<u32>,
164 free: Vec<Vec<u32>>,
165 both: bool,
166}
167
168impl Default for Adjacency {
169 fn default() -> Adjacency {
170 Adjacency::new()
171 }
172}
173
174impl Adjacency {
175 /// An empty plane that indexes both directions, so `In` answers as well as
176 /// `Out` does.
177 #[must_use]
178 pub fn new() -> Adjacency {
179 Adjacency::build(true)
180 }
181
182 /// An empty plane that indexes outgoing edges only.
183 ///
184 /// This halves the memory and halves the work an insert does, and it is the
185 /// right choice whenever nothing asks the graph who points at a node.
186 /// `neighbours` under [`Dir::In`] then answers nothing at all, which is why
187 /// it is a decision at construction rather than a flag on a call: a walk
188 /// that silently found no incoming edges because of how the plane was built
189 /// would look exactly like a node that has none.
190 #[must_use]
191 pub fn out_only() -> Adjacency {
192 Adjacency::build(false)
193 }
194
195 fn build(both: bool) -> Adjacency {
196 Adjacency {
197 slots: Vec::new(),
198 live: 0,
199 filled: 0,
200 edges: 0,
201 entries: 0,
202 neighbour: Vec::new(),
203 edge: Vec::new(),
204 free: Vec::new(),
205 both,
206 }
207 }
208
209 /// Whether incoming edges are indexed.
210 #[must_use]
211 pub fn indexes_incoming(&self) -> bool {
212 self.both
213 }
214
215 /// How many edges have been linked and not unlinked.
216 #[must_use]
217 pub fn edges(&self) -> usize {
218 self.edges
219 }
220
221 /// Whether any edge is linked.
222 #[must_use]
223 pub fn is_empty(&self) -> bool {
224 self.edges == 0
225 }
226
227 /// How many runs hold at least one edge.
228 #[must_use]
229 pub fn runs(&self) -> usize {
230 self.filled
231 }
232
233 /// Add an edge from `src` to `dst` under `label`, carrying `edge` as the
234 /// slot of the edge record.
235 ///
236 /// This appends. It does not look for an edge that is already there, for
237 /// the reason in the module docs, so linking the same pair twice leaves two
238 /// entries and unlinking it once leaves one.
239 pub fn link(&mut self, src: u64, dst: u64, label: u32, edge: u32) {
240 let s = self.run_for(src, label, 0);
241 self.push(s, dst, edge);
242 if self.both {
243 let d = self.run_for(dst, label, INCOMING);
244 self.push(d, src, edge);
245 }
246 self.edges += 1;
247 }
248
249 /// Remove one edge from `src` to `dst` under `label`, and answer with the
250 /// edge slot it was carrying.
251 ///
252 /// Costs a scan of the run at each end, because finding which position an
253 /// edge sits at is the one thing a plane keyed by node rather than by edge
254 /// cannot do in a step. A caller that already knows the position wants
255 /// [`Adjacency::unlink_at`].
256 pub fn unlink(&mut self, src: u64, dst: u64, label: u32) -> Option<u32> {
257 let s = self.find(src, label, 0)?;
258 let i = self.position(s, dst)?;
259 let edge = self.take(s, i).1;
260 if self.both
261 && let Some(d) = self.find(dst, label, INCOMING)
262 && let Some(j) = self.position(d, src)
263 {
264 self.take(d, j);
265 }
266 self.edges -= 1;
267 Some(edge)
268 }
269
270 /// Remove the edge at position `i` of one run, and answer with the
271 /// neighbour and the edge slot that were there.
272 ///
273 /// This is the O(1) primitive and it touches one end only, so the other end
274 /// still holds its half of the edge. It is for a caller that tracks
275 /// positions itself and will do both. Whatever used to be last has moved
276 /// into `i`.
277 pub fn unlink_at(&mut self, node: u64, label: u32, dir: Dir, i: usize) -> Option<(u64, u32)> {
278 let s = self.find(node, label, incoming(dir))?;
279 if i >= self.slots[s].len as usize {
280 return None;
281 }
282 Some(self.take(s, i))
283 }
284
285 /// The neighbours of `node` under `label` in `dir`, in one contiguous run.
286 ///
287 /// One probe and then a sequential read. The order is whatever inserting
288 /// and deleting left behind, because a delete moves the last entry into the
289 /// hole it made.
290 #[must_use]
291 pub fn neighbours(&self, node: u64, label: u32, dir: Dir) -> &[u64] {
292 match self.find(node, label, incoming(dir)) {
293 Some(s) => {
294 let (at, len) = (self.slots[s].at as usize, self.slots[s].len as usize);
295 &self.neighbour[at..at + len]
296 }
297 None => &[],
298 }
299 }
300
301 /// The edge slots of `node` under `label` in `dir`, lined up one for one
302 /// with [`Adjacency::neighbours`].
303 #[must_use]
304 pub fn edge_slots(&self, node: u64, label: u32, dir: Dir) -> &[u32] {
305 match self.find(node, label, incoming(dir)) {
306 Some(s) => {
307 let (at, len) = (self.slots[s].at as usize, self.slots[s].len as usize);
308 &self.edge[at..at + len]
309 }
310 None => &[],
311 }
312 }
313
314 /// How many edges `node` has under `label` in `dir`.
315 #[must_use]
316 pub fn degree(&self, node: u64, label: u32, dir: Dir) -> usize {
317 match self.find(node, label, incoming(dir)) {
318 Some(s) => self.slots[s].len as usize,
319 None => 0,
320 }
321 }
322
323 /// Every non empty run under `label` in `dir`, as the node and its two
324 /// slices, in whatever order the table happens to hold them.
325 ///
326 /// This is the read side of promotion. The cold form is built by walking
327 /// the hot plane once and handing every run to an encoder, and there is no
328 /// other way to get at a run whose node you have not already been told
329 /// about, because the table is keyed by the node rather than ordered by it.
330 /// The order is deliberately not promised: a caller that needs the runs in
331 /// node order is building something sorted anyway and can sort what it
332 /// collects.
333 pub fn for_each_run(&self, label: u32, dir: Dir, mut f: impl FnMut(u64, &[u64], &[u32])) {
334 let want = incoming(dir);
335 for s in &self.slots {
336 if s.flags & LIVE == 0 || s.len == 0 || s.label != label || s.flags & INCOMING != want {
337 continue;
338 }
339 let (at, len) = (s.at as usize, s.len as usize);
340 f(
341 s.node,
342 &self.neighbour[at..at + len],
343 &self.edge[at..at + len],
344 );
345 }
346 }
347
348 /// Ask the cache for the slot a run's header will be found in.
349 ///
350 /// A multi hop walk knows its whole next frontier before it reads any of
351 /// it, so it can issue these across the frontier and then come back and
352 /// read. That is the same two walk shape `04` section 3 drains a command
353 /// batch with, and it is what the two hop budget in G14 is actually
354 /// spending: the probes are dependent loads, and the only way to make them
355 /// cheap is to stop them being serial.
356 pub fn prefetch(&self, node: u64, label: u32, dir: Dir) {
357 if self.slots.is_empty() {
358 return;
359 }
360 let i = bucket(hash(node, label, incoming(dir)), self.slots.len());
361 prefetch(&self.slots[i]);
362 }
363
364 /// Resident bytes, counting the table, both arenas, and everything the free
365 /// lists are still holding.
366 #[must_use]
367 pub fn bytes(&self) -> usize {
368 self.slots.capacity() * size_of::<Slot>()
369 + self.neighbour.capacity() * size_of::<u64>()
370 + self.edge.capacity() * size_of::<u32>()
371 + self.free.capacity() * size_of::<Vec<u32>>()
372 + self
373 .free
374 .iter()
375 .map(|f| f.capacity() * size_of::<u32>())
376 .sum::<usize>()
377 }
378
379 /// Rebuild the table and the arenas with nothing spare in them.
380 ///
381 /// A run that emptied leaves its header behind, a run that shrank leaves
382 /// slack, and a free list holds blocks nothing has asked for again. None of
383 /// that is worth chasing on the write path, so this is the sweep that
384 /// reclaims it, and it is the natural thing to run before a settled part of
385 /// the graph is promoted into the cold form. Every run comes out sized to
386 /// exactly what it holds and laid out one after another, which is also the
387 /// order the promotion wants to read them in.
388 pub fn compact(&mut self) {
389 let mut keep: Vec<Slot> = self
390 .slots
391 .iter()
392 .copied()
393 .filter(|s| s.flags & LIVE != 0 && s.len > 0)
394 .collect();
395 let mut neighbour = Vec::with_capacity(self.entries);
396 let mut edge = Vec::with_capacity(self.entries);
397 for slot in &mut keep {
398 let (at, len) = (slot.at as usize, slot.len as usize);
399 let to = neighbour.len() as u32;
400 neighbour.extend_from_slice(&self.neighbour[at..at + len]);
401 edge.extend_from_slice(&self.edge[at..at + len]);
402 slot.at = to;
403 slot.cap = slot.len;
404 }
405 self.neighbour = neighbour;
406 self.edge = edge;
407 self.free = Vec::new();
408 self.live = keep.len();
409 self.filled = keep.len();
410 // Sized off the live count rather than rounded up to a power of two,
411 // which is the whole reason the table is indexed by a multiply instead
412 // of a mask. A graph with two hundred thousand runs would otherwise get
413 // half a million slots and carry three bytes an edge it never uses.
414 self.slots = vec![Slot::default(); (keep.len() * 4 / 3).max(16)];
415 for slot in keep {
416 let i = self.vacancy(slot.node, slot.label, slot.flags & INCOMING);
417 self.slots[i] = slot;
418 }
419 }
420
421 fn find(&self, node: u64, label: u32, incoming: u8) -> Option<usize> {
422 if self.slots.is_empty() {
423 return None;
424 }
425 let n = self.slots.len();
426 let mut i = bucket(hash(node, label, incoming), n);
427 loop {
428 let s = &self.slots[i];
429 if s.flags & LIVE == 0 {
430 return None;
431 }
432 if s.node == node && s.label == label && s.flags & INCOMING == incoming {
433 return Some(i);
434 }
435 i += 1;
436 if i == n {
437 i = 0;
438 }
439 }
440 }
441
442 fn position(&self, s: usize, node: u64) -> Option<usize> {
443 let (at, len) = (self.slots[s].at as usize, self.slots[s].len as usize);
444 self.neighbour[at..at + len].iter().position(|n| *n == node)
445 }
446
447 /// The slot for a run, made if it was not there.
448 fn run_for(&mut self, node: u64, label: u32, incoming: u8) -> usize {
449 if (self.live + 1) * 4 > self.slots.len() * 3 {
450 self.regrow();
451 }
452 let n = self.slots.len();
453 let mut i = bucket(hash(node, label, incoming), n);
454 loop {
455 let s = &self.slots[i];
456 if s.flags & LIVE == 0 {
457 self.slots[i] = Slot {
458 node,
459 at: 0,
460 len: 0,
461 cap: 0,
462 label,
463 flags: LIVE | incoming,
464 };
465 self.live += 1;
466 return i;
467 }
468 if s.node == node && s.label == label && s.flags & INCOMING == incoming {
469 return i;
470 }
471 i += 1;
472 if i == n {
473 i = 0;
474 }
475 }
476 }
477
478 /// Where a key that is known not to be there belongs.
479 fn vacancy(&self, node: u64, label: u32, incoming: u8) -> usize {
480 let n = self.slots.len();
481 let mut i = bucket(hash(node, label, incoming), n);
482 while self.slots[i].flags & LIVE != 0 {
483 i += 1;
484 if i == n {
485 i = 0;
486 }
487 }
488 i
489 }
490
491 fn regrow(&mut self) {
492 // A quarter more rather than double, for the same reason the table is
493 // not a power of two. Growth by a factor g leaves the load factor
494 // wandering between 0.75 and 0.75 over g, so doubling means half the
495 // table is empty for most of its life. A quarter holds it between 0.6
496 // and 0.75, and the price is four rehashes per run over the whole
497 // build rather than two.
498 let want = (self.slots.len() + self.slots.len() / 4).max(16);
499 let old = core::mem::replace(&mut self.slots, vec![Slot::default(); want]);
500 for slot in old {
501 if slot.flags & LIVE != 0 {
502 let i = self.vacancy(slot.node, slot.label, slot.flags & INCOMING);
503 self.slots[i] = slot;
504 }
505 }
506 }
507
508 fn push(&mut self, s: usize, node: u64, edge: u32) {
509 let Slot {
510 mut at,
511 len,
512 mut cap,
513 ..
514 } = self.slots[s];
515 if len == cap {
516 let want = LADDER[ceil_class(cap + 1)];
517 let to = self.alloc(want);
518 if len > 0 {
519 self.copy_run(at, to, len as usize);
520 self.release(at, cap);
521 }
522 at = to;
523 cap = want;
524 }
525 let i = at as usize + len as usize;
526 self.neighbour[i] = node;
527 self.edge[i] = edge;
528 self.slots[s].at = at;
529 self.slots[s].cap = cap;
530 self.slots[s].len = len + 1;
531 self.entries += 1;
532 if len == 0 {
533 self.filled += 1;
534 }
535 }
536
537 /// Swap with last, decrement, and give back capacity the run has outgrown.
538 fn take(&mut self, s: usize, i: usize) -> (u64, u32) {
539 let Slot { at, len, cap, .. } = self.slots[s];
540 let (at, last) = (at as usize, at as usize + len as usize - 1);
541 let gone = (self.neighbour[at + i], self.edge[at + i]);
542 self.neighbour[at + i] = self.neighbour[last];
543 self.edge[at + i] = self.edge[last];
544 self.slots[s].len = len - 1;
545 self.entries -= 1;
546 if len == 1 {
547 self.filled -= 1;
548 }
549 self.shrink(s, cap);
550 gone
551 }
552
553 fn shrink(&mut self, s: usize, cap: u32) {
554 let len = self.slots[s].len;
555 if len == 0 {
556 self.release(self.slots[s].at, cap);
557 self.slots[s].at = 0;
558 self.slots[s].cap = 0;
559 return;
560 }
561 // Half of the capacity is the trigger and the smallest size that fits
562 // is the destination, so a run has to lose half of itself before it is
563 // moved and it will not be moved again until it has doubled.
564 if len * 2 > cap {
565 return;
566 }
567 let want = LADDER[ceil_class(len)];
568 if want >= cap {
569 return;
570 }
571 let at = self.slots[s].at;
572 let to = self.alloc(want);
573 self.copy_run(at, to, len as usize);
574 self.release(at, cap);
575 self.slots[s].at = to;
576 self.slots[s].cap = want;
577 }
578
579 fn copy_run(&mut self, from: u32, to: u32, len: usize) {
580 let (from, to) = (from as usize, to as usize);
581 self.neighbour.copy_within(from..from + len, to);
582 self.edge.copy_within(from..from + len, to);
583 }
584
585 fn alloc(&mut self, cap: u32) -> u32 {
586 let class = ceil_class(cap);
587 if let Some(list) = self.free.get_mut(class)
588 && let Some(at) = list.pop()
589 {
590 return at;
591 }
592 let cap = cap as usize;
593 let at = self.neighbour.len();
594 assert!(at + cap <= u32::MAX as usize, "the adjacency arena is full");
595 // An eighth more when it has to grow, not double. These are the
596 // biggest things here by a long way, so a doubling that lands one edge
597 // past the last one leaves half the plane allocated and never touched.
598 // Nothing is rehashed on the way, it is one copy, so the growth can be
599 // much finer here than the table's.
600 if at + cap > self.neighbour.capacity() {
601 let cur = self.neighbour.capacity();
602 let want = (cur + cur / 8).max(at + cap).max(64);
603 self.neighbour.reserve_exact(want - at);
604 self.edge.reserve_exact(want - at);
605 }
606 self.neighbour.resize(at + cap, 0);
607 self.edge.resize(at + cap, 0);
608 at as u32
609 }
610
611 /// A block goes back under the largest size that certainly fits inside it,
612 /// so a block that came out of a compaction at some exact length is still
613 /// reusable and is never handed out as more room than it has.
614 fn release(&mut self, at: u32, cap: u32) {
615 let class = floor_class(cap);
616 while self.free.len() <= class {
617 self.free.push(Vec::new());
618 }
619 self.free[class].push(at);
620 }
621}
622
623/// The smallest ladder size that holds `n` edges.
624///
625/// A linear walk, because the answer is in the first few entries for nearly
626/// every node in a real graph and a binary search over 96 entries would be
627/// slower for the case that matters.
628fn ceil_class(n: u32) -> usize {
629 LADDER.iter().position(|c| *c >= n).unwrap_or(CLASSES - 1)
630}
631
632/// The largest ladder size that fits inside `n` edges.
633fn floor_class(n: u32) -> usize {
634 let at = ceil_class(n);
635 if LADDER[at] > n {
636 at.saturating_sub(1)
637 } else {
638 at
639 }
640}
641
642fn incoming(dir: Dir) -> u8 {
643 match dir {
644 Dir::Out => 0,
645 Dir::In => INCOMING,
646 }
647}
648
649/// Lemire's reduction: the top bits of the hash scaled onto `n`, so the table
650/// can be any size at all rather than a power of two. That matters more here
651/// than the one multiply costs, because rounding a run count up to a power of
652/// two is up to twice the table for nothing.
653#[inline]
654fn bucket(h: u64, n: usize) -> usize {
655 ((u128::from(h) * n as u128) >> 64) as usize
656}
657
658/// Node ids are usually dense small integers and there are only ever a handful
659/// of distinct labels, so the label and the direction are spread across the
660/// whole word before the finaliser rather than after it. Without that, every
661/// label of one node lands in a run of neighbouring slots and a probe for one
662/// walks over the others.
663#[inline]
664fn hash(node: u64, label: u32, incoming: u8) -> u64 {
665 let tag = (u64::from(label) << 1) | u64::from(incoming >> 1);
666 let mut x = node ^ tag.wrapping_mul(0x9e37_79b9_7f4a_7c15);
667 x ^= x >> 33;
668 x = x.wrapping_mul(0xff51_afd7_ed55_8ccd);
669 x ^= x >> 29;
670 x = x.wrapping_mul(0xc4ce_b9fe_1a85_ec53);
671 x ^ (x >> 32)
672}
673
674#[cfg(test)]
675mod tests {
676 use super::*;
677 use yo_common::Rng;
678
679 const FOLLOWS: u32 = 1;
680 const WORKS_AT: u32 = 2;
681
682 fn sorted(v: &[u64]) -> Vec<u64> {
683 let mut v = v.to_vec();
684 v.sort_unstable();
685 v
686 }
687
688 #[test]
689 fn a_slot_is_one_half_of_a_cache_line() {
690 assert_eq!(size_of::<Slot>(), 32);
691 }
692
693 #[test]
694 fn the_ladder_only_ever_goes_up() {
695 for w in LADDER.windows(2) {
696 assert!(w[1] > w[0] || w[0] == u32::MAX, "{w:?} does not go up");
697 }
698 assert_eq!(LADDER[4], 16, "doubling should run out at 16");
699 assert_eq!(
700 LADDER[5], 20,
701 "and a quarter more should be the step after it"
702 );
703 assert!(
704 LADDER.contains(&u32::MAX),
705 "the ladder should reach the end of a u32"
706 );
707 // The two ends of a size, which is what the free list keys on.
708 assert_eq!(LADDER[ceil_class(17)], 20);
709 assert_eq!(LADDER[floor_class(19)], 16);
710 assert_eq!(LADDER[floor_class(20)], 20);
711 assert_eq!(LADDER[ceil_class(1)], 1);
712 }
713
714 #[test]
715 fn a_run_is_the_neighbours_that_were_linked_to_it() {
716 let mut g = Adjacency::new();
717 for (i, dst) in [7u64, 9, 11].iter().enumerate() {
718 g.link(1, *dst, FOLLOWS, i as u32);
719 }
720 assert_eq!(g.neighbours(1, FOLLOWS, Dir::Out), &[7, 9, 11]);
721 assert_eq!(g.edge_slots(1, FOLLOWS, Dir::Out), &[0, 1, 2]);
722 assert_eq!(g.degree(1, FOLLOWS, Dir::Out), 3);
723 assert_eq!(g.edges(), 3);
724 assert_eq!(g.runs(), 4);
725 }
726
727 #[test]
728 fn a_node_nobody_linked_has_no_neighbours_rather_than_no_answer() {
729 let g = Adjacency::new();
730 assert!(g.neighbours(1, FOLLOWS, Dir::Out).is_empty());
731 assert!(g.edge_slots(1, FOLLOWS, Dir::Out).is_empty());
732 assert_eq!(g.degree(1, FOLLOWS, Dir::Out), 0);
733 assert!(g.is_empty());
734 g.prefetch(1, FOLLOWS, Dir::Out);
735 }
736
737 #[test]
738 fn an_edge_is_readable_from_both_ends() {
739 let mut g = Adjacency::new();
740 g.link(1, 2, FOLLOWS, 10);
741 assert_eq!(g.neighbours(1, FOLLOWS, Dir::Out), &[2]);
742 assert_eq!(g.neighbours(2, FOLLOWS, Dir::In), &[1]);
743 assert_eq!(g.edge_slots(2, FOLLOWS, Dir::In), &[10]);
744 assert!(g.neighbours(2, FOLLOWS, Dir::Out).is_empty());
745 }
746
747 #[test]
748 fn one_label_is_not_another() {
749 let mut g = Adjacency::new();
750 g.link(1, 2, FOLLOWS, 10);
751 g.link(1, 3, WORKS_AT, 11);
752 assert_eq!(g.neighbours(1, FOLLOWS, Dir::Out), &[2]);
753 assert_eq!(g.neighbours(1, WORKS_AT, Dir::Out), &[3]);
754 }
755
756 #[test]
757 fn a_self_loop_is_at_both_of_its_ends_and_they_are_different_runs() {
758 let mut g = Adjacency::new();
759 g.link(1, 1, FOLLOWS, 5);
760 assert_eq!(g.neighbours(1, FOLLOWS, Dir::Out), &[1]);
761 assert_eq!(g.neighbours(1, FOLLOWS, Dir::In), &[1]);
762 assert_eq!(g.unlink(1, 1, FOLLOWS), Some(5));
763 assert!(g.neighbours(1, FOLLOWS, Dir::Out).is_empty());
764 assert!(g.neighbours(1, FOLLOWS, Dir::In).is_empty());
765 }
766
767 #[test]
768 fn out_only_stores_nothing_incoming() {
769 let mut both = Adjacency::new();
770 let mut out = Adjacency::out_only();
771 // A chain, and the claim is about one node in the middle of it, so
772 // the chain only has to be long enough to have a middle.
773 let n = if cfg!(miri) { 100u64 } else { 1000 };
774 let mid = n / 2;
775 for i in 0..n {
776 both.link(i, i + 1, FOLLOWS, i as u32);
777 out.link(i, i + 1, FOLLOWS, i as u32);
778 }
779 assert_eq!(out.neighbours(mid, FOLLOWS, Dir::Out), &[mid + 1]);
780 assert!(out.neighbours(mid, FOLLOWS, Dir::In).is_empty());
781 assert_eq!(both.neighbours(mid, FOLLOWS, Dir::In), &[mid - 1]);
782 assert!(!out.indexes_incoming());
783 assert!(
784 out.bytes() * 3 < both.bytes() * 2,
785 "{} against {}",
786 out.bytes(),
787 both.bytes()
788 );
789 }
790
791 #[test]
792 fn unlinking_takes_the_edge_off_both_ends() {
793 let mut g = Adjacency::new();
794 g.link(1, 2, FOLLOWS, 10);
795 g.link(1, 3, FOLLOWS, 11);
796 assert_eq!(g.unlink(1, 2, FOLLOWS), Some(10));
797 assert_eq!(g.neighbours(1, FOLLOWS, Dir::Out), &[3]);
798 assert!(g.neighbours(2, FOLLOWS, Dir::In).is_empty());
799 assert_eq!(g.edges(), 1);
800 assert_eq!(g.unlink(1, 2, FOLLOWS), None);
801 assert_eq!(g.unlink(9, 9, FOLLOWS), None);
802 }
803
804 #[test]
805 fn a_delete_moves_the_last_edge_into_the_hole() {
806 let mut g = Adjacency::new();
807 for dst in 1..=5u64 {
808 g.link(0, dst, FOLLOWS, dst as u32);
809 }
810 g.unlink(0, 2, FOLLOWS);
811 // The order is gone but nothing else is, and the edge slot went with
812 // the neighbour it belonged to.
813 let n = g.neighbours(0, FOLLOWS, Dir::Out);
814 let e = g.edge_slots(0, FOLLOWS, Dir::Out);
815 assert_eq!(sorted(n), vec![1, 3, 4, 5]);
816 for (i, node) in n.iter().enumerate() {
817 assert_eq!(u64::from(e[i]), *node, "the pairing survived the swap");
818 }
819 }
820
821 #[test]
822 fn unlink_at_moves_the_last_entry_into_the_position_it_took() {
823 let mut g = Adjacency::new();
824 for dst in 1..=4u64 {
825 g.link(0, dst, FOLLOWS, dst as u32);
826 }
827 assert_eq!(g.unlink_at(0, FOLLOWS, Dir::Out, 0), Some((1, 1)));
828 assert_eq!(g.neighbours(0, FOLLOWS, Dir::Out), &[4, 2, 3]);
829 assert_eq!(g.unlink_at(0, FOLLOWS, Dir::Out, 9), None);
830 assert_eq!(g.unlink_at(7, FOLLOWS, Dir::Out, 0), None);
831 }
832
833 #[test]
834 fn a_run_survives_growing_through_every_size_it_passes() {
835 let mut g = Adjacency::out_only();
836 // Fewer sizes under Miri, not none: the ladder is geometric, so six
837 // hundred still climbs most of the classes five thousand does and the
838 // last few are the ones a run of this shape never reaches anyway.
839 let n = if cfg!(miri) { 600u64 } else { 5000 };
840 for dst in 0..n {
841 g.link(0, dst, FOLLOWS, dst as u32);
842 }
843 assert_eq!(g.degree(0, FOLLOWS, Dir::Out), n as usize);
844 assert_eq!(
845 g.neighbours(0, FOLLOWS, Dir::Out),
846 (0..n).collect::<Vec<_>>()
847 );
848 assert_eq!(
849 g.edge_slots(0, FOLLOWS, Dir::Out),
850 (0..n as u32).collect::<Vec<_>>()
851 );
852 }
853
854 #[test]
855 fn a_hub_that_empties_gives_its_block_back() {
856 let mut g = Adjacency::out_only();
857 // What is checked is that the second hub is free, not that either hub
858 // is any particular size, so both come down together and the claim is
859 // the same comparison between the same two states.
860 let n = if cfg!(miri) { 400u64 } else { 4000 };
861 for dst in 0..n {
862 g.link(0, dst, FOLLOWS, 0);
863 }
864 let full = g.bytes();
865 for dst in 0..n {
866 assert!(g.unlink(0, dst, FOLLOWS).is_some());
867 }
868 assert_eq!(g.degree(0, FOLLOWS, Dir::Out), 0);
869 assert_eq!(g.runs(), 0);
870 // Filling a second node to the same size reuses what the first gave
871 // back rather than asking the arena for more.
872 for dst in 0..n {
873 g.link(1, dst, FOLLOWS, 0);
874 }
875 assert!(g.bytes() <= full + full / 4, "{} against {full}", g.bytes());
876 }
877
878 #[test]
879 fn a_run_that_grows_and_shrinks_does_not_copy_itself_on_a_boundary() {
880 // Sixteen is the last doubling, so this sits astride it. What is
881 // checked is that the capacity settles rather than that anything is
882 // fast: an implementation that shrank on the exact fit would move the
883 // run on every one of these, and the arena would grow without end.
884 let mut g = Adjacency::out_only();
885 for dst in 0..16u64 {
886 g.link(0, dst, FOLLOWS, 0);
887 }
888 // The first cycle does grow the run once, from the 16 it fits exactly
889 // into to the 20 above it. Everything after that is the claim.
890 g.link(0, 999, FOLLOWS, 0);
891 g.unlink(0, 999, FOLLOWS);
892 let settled = g.bytes();
893 for _ in 0..100 {
894 g.link(0, 999, FOLLOWS, 0);
895 g.unlink(0, 999, FOLLOWS);
896 }
897 assert_eq!(g.bytes(), settled);
898 assert_eq!(g.degree(0, FOLLOWS, Dir::Out), 16);
899 }
900
901 #[test]
902 fn a_run_that_loses_most_of_itself_gives_the_room_back() {
903 let mut g = Adjacency::out_only();
904 // Ten survivors out of however many went in, which is the ratio the
905 // claim is about. The ten is written once and the rest follows from it.
906 let n = if cfg!(miri) { 400u64 } else { 4000 };
907 for dst in 0..n {
908 g.link(0, dst, FOLLOWS, 0);
909 }
910 for dst in 0..n - 10 {
911 g.unlink(0, dst, FOLLOWS);
912 }
913 assert_eq!(g.degree(0, FOLLOWS, Dir::Out), 10);
914 // The run itself came down on the way, without waiting for a sweep.
915 assert!(g.slots.iter().any(|s| s.len == 10 && s.cap <= 16));
916 g.compact();
917 assert_eq!(g.degree(0, FOLLOWS, Dir::Out), 10);
918 assert!(g.bytes() < 4000, "{} bytes for ten edges", g.bytes());
919 }
920
921 #[test]
922 fn compact_drops_the_runs_that_emptied() {
923 let mut g = Adjacency::new();
924 // Ten edges left out of however many, so there are twenty runs to find
925 // among a great many that emptied. Every index below is derived from
926 // `n`, because a number written twice stops meaning anything the moment
927 // one of the two moves.
928 let n = if cfg!(miri) { 400u64 } else { 2000 };
929 let live = 10;
930 for i in 0..n {
931 g.link(i, i + 1, FOLLOWS, i as u32);
932 }
933 for i in 0..n - live {
934 g.unlink(i, i + 1, FOLLOWS);
935 }
936 assert_eq!(g.runs(), 20);
937 let before = g.bytes();
938 g.compact();
939 assert_eq!(g.runs(), 20);
940 assert_eq!(g.edges(), live as usize);
941 let mid = n - live / 2;
942 assert_eq!(g.neighbours(mid, FOLLOWS, Dir::Out), &[mid + 1]);
943 assert_eq!(g.neighbours(mid + 1, FOLLOWS, Dir::In), &[mid]);
944 assert!(g.bytes() * 4 < before, "{} against {before}", g.bytes());
945 // And it is still a working plane afterwards, which is the part a
946 // rebuild is easy to get wrong. This one also has to grow a run whose
947 // capacity the sweep cut to exactly what it held.
948 let fresh = n * 2;
949 g.link(mid, fresh, FOLLOWS, 7);
950 assert_eq!(
951 sorted(g.neighbours(mid, FOLLOWS, Dir::Out)),
952 vec![mid + 1, fresh]
953 );
954 assert_eq!(sorted(g.neighbours(fresh, FOLLOWS, Dir::In)), vec![mid]);
955 }
956
957 // Not shrunk. The claim is bytes an edge on a degree distribution with a
958 // tail, and both halves of it need the size: the run headers only average
959 // out over a lot of runs, and the fat tail only appears at all once there
960 // are enough nodes for two percent of them to be hubs. A smaller version
961 // measures a different structure and would pass or fail for its own
962 // reasons.
963 #[cfg_attr(miri, ignore = "bytes an edge is the claim and it needs the graph")]
964 #[test]
965 fn a_hot_run_costs_about_twelve_bytes_an_edge() {
966 // A degree distribution with a tail, because a uniform one hides both
967 // things that could go wrong: the run headers, which a graph of hubs
968 // has too few of to notice, and the capacity slack, which a graph of
969 // leaves never reaches.
970 let mut g = Adjacency::out_only();
971 let mut rng = Rng::new(0x9e3f);
972 let nodes = 200_000u64;
973 let mut edges = 0usize;
974 for src in 0..nodes {
975 let deg = match rng.next_u64() % 1000 {
976 0..=799 => 1 + rng.next_u64() % 4,
977 800..=979 => 5 + rng.next_u64() % 40,
978 _ => 45 + rng.next_u64() % 600,
979 };
980 for _ in 0..deg {
981 g.link(src, rng.next_u64() % nodes, FOLLOWS, 0);
982 edges += 1;
983 }
984 }
985 let per = g.bytes() as f64 / edges as f64;
986 g.compact();
987 let settled = g.bytes() as f64 / edges as f64;
988 // Twelve is the payload and the rest is one 32 byte header per run
989 // against an average degree in the teens, plus the capacity slack.
990 // Both are the price of a structure that inserts and deletes in
991 // constant time, and the cold form is where 8 bits an edge comes from.
992 assert!(per < 19.0, "{per:.2} bytes an edge over {edges} edges");
993 assert!(settled < 16.0, "{settled:.2} bytes an edge once swept");
994 }
995
996 #[test]
997 fn a_two_hop_reaches_what_a_pair_of_one_hops_reaches() {
998 let mut g = Adjacency::new();
999 let mut rng = Rng::new(7);
1000 // The walk is over one node's neighbours and their neighbours, so what
1001 // it costs is the degree and not the graph. The graph only has to be
1002 // wide enough that the eight hops land on eight different nodes.
1003 let nodes = if cfg!(miri) { 300u64 } else { 5000 };
1004 for src in 0..nodes {
1005 for _ in 0..8 {
1006 g.link(src, rng.next_u64() % nodes, FOLLOWS, 0);
1007 }
1008 }
1009 let first = g.neighbours(0, FOLLOWS, Dir::Out).to_vec();
1010 for hop in &first {
1011 g.prefetch(*hop, FOLLOWS, Dir::Out);
1012 }
1013 let mut seen = Vec::new();
1014 for hop in &first {
1015 seen.extend_from_slice(g.neighbours(*hop, FOLLOWS, Dir::Out));
1016 }
1017 assert_eq!(seen.len(), 64);
1018 // Every edge is at both ends, so everything the walk reached agrees it
1019 // was reached from where the walk was standing.
1020 for (i, hop) in first.iter().enumerate() {
1021 for dst in &seen[i * 8..(i + 1) * 8] {
1022 assert!(g.neighbours(*dst, FOLLOWS, Dir::In).contains(hop));
1023 }
1024 }
1025 }
1026
1027 #[test]
1028 fn the_plane_agrees_with_a_list_of_what_was_done_to_it() {
1029 // The reference is a plain vector per node, which is obviously right
1030 // and obviously too expensive, and the point is that the plane matches
1031 // it over a mix of links and unlinks that crosses every capacity size
1032 // in both directions.
1033 let mut g = Adjacency::new();
1034 let mut want: Vec<Vec<u64>> = vec![Vec::new(); 64];
1035 let mut rng = Rng::new(0xbeef);
1036 // Sixty four nodes either way, so the mix still crosses every capacity
1037 // size in both directions. It is the operations that come down, and
1038 // four thousand of them over sixty four nodes is still an average of
1039 // sixty apiece, which is well past the last doubling.
1040 let ops = if cfg!(miri) { 2_000 } else { 200_000 };
1041 for _ in 0..ops {
1042 let src = rng.next_u64() % 64;
1043 let dst = rng.next_u64() % 64;
1044 if rng.next_u64().is_multiple_of(3) {
1045 if let Some(i) = want[src as usize].iter().position(|n| *n == dst) {
1046 want[src as usize].swap_remove(i);
1047 assert!(g.unlink(src, dst, FOLLOWS).is_some());
1048 } else {
1049 assert_eq!(g.unlink(src, dst, FOLLOWS), None);
1050 }
1051 } else {
1052 want[src as usize].push(dst);
1053 g.link(src, dst, FOLLOWS, 0);
1054 }
1055 }
1056 let mut total = 0;
1057 for (src, list) in want.iter().enumerate() {
1058 assert_eq!(
1059 sorted(g.neighbours(src as u64, FOLLOWS, Dir::Out)),
1060 sorted(list),
1061 "node {src}"
1062 );
1063 total += list.len();
1064 }
1065 assert_eq!(g.edges(), total);
1066 // And the incoming side is the transpose of the outgoing one.
1067 let mut incoming: Vec<Vec<u64>> = vec![Vec::new(); 64];
1068 for (src, list) in want.iter().enumerate() {
1069 for dst in list {
1070 incoming[*dst as usize].push(src as u64);
1071 }
1072 }
1073 for (dst, list) in incoming.iter().enumerate() {
1074 assert_eq!(
1075 sorted(g.neighbours(dst as u64, FOLLOWS, Dir::In)),
1076 sorted(list),
1077 "into node {dst}"
1078 );
1079 }
1080 }
1081}