From 1209d75e15553de5c8c4bd8c9e80df7d142e2545 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Thu, 3 Sep 2026 20:28:41 +0200 Subject: [PATCH] Use non-zero edge indices to reduce size of structs with Option fields --- src/models/append_graph.rs | 41 ++++++++++++++++++++++++-------------- src/models/graph.rs | 25 ++++++++++++++++------- 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/src/models/append_graph.rs b/src/models/append_graph.rs index 47f26d5..92adb4d 100644 --- a/src/models/append_graph.rs +++ b/src/models/append_graph.rs @@ -1,5 +1,7 @@ //! [`AppendGraph`], an undirected graph topology supporting addition only. +use std::num::NonZeroUsize; + use crate::maps::ElementMap; use crate::traits::{GraphTopology, GraphTopologyAddition, Incidence, IncidenceCursor}; @@ -10,12 +12,27 @@ use crate::traits::{GraphTopology, GraphTopologyAddition, Incidence, IncidenceCu #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)] pub struct Vertex(usize); +// TODO: Benchmark the cost of the index shift. An alternative could be to init AppendGraph::incidences with two dummy entries to avoid 0. /// An opaque handle identifying an edge in an [`AppendGraph`]. /// /// Handles are stable for the lifetime of the graph. Obtain via graph methods like /// [`AppendGraph::add_edge`] and [`AppendGraph::edges`]. #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)] -pub struct Edge(usize); +pub struct Edge(NonZeroUsize); + +impl Edge { + fn new(index: usize) -> Self { + Edge(NonZeroUsize::new(index + 1).unwrap()) + } + + fn index(self) -> usize { + self.0.get() - 1 + } + + fn normalize(&self) -> Self { + Self::new(self.index() & !1) + } +} /// An [`ElementMap`] for [`AppendGraph`] vertices. pub type AppendGraphVertexMap = ElementMap; @@ -26,13 +43,6 @@ pub type AppendGraphEdgeMap = ElementMap; /// An [`Incidence`] for [`AppendGraph`]. pub type AppendGraphIncidence = Incidence; -impl Edge { - fn normalize(&self) -> Self { - Self(self.0 & !1) - } -} - -// TODO: Check if VertexIncidenceHeader and IncidenceEntry can be made smaller. Currently they both take 24 bytes (on 64bit), see https://stackoverflow.com/a/79653173 struct VertexIncidenceHeader { incidence_count: usize, first_incidence: Option, @@ -118,7 +128,7 @@ impl AppendGraph { adjacent: v2, }); self.vertices[v1.0].incidence_count += 1; - self.vertices[v1.0].first_incidence = Some(Edge(self.incidences.len() - 1)); + self.vertices[v1.0].first_incidence = Some(Edge::new(self.incidences.len() - 1)); } fn raw_incidences(&self, v: Vertex) -> impl Iterator { @@ -128,7 +138,7 @@ impl AppendGraph { fn step_incidence(&self, incidence: &mut Option) -> Option<(Vertex, Edge)> { let current = (*incidence)?; - let entry = self.incidences[current.0]; + let entry = self.incidences[current.index()]; *incidence = entry.next; Some((entry.adjacent, current)) } @@ -160,7 +170,7 @@ impl GraphTopology for AppendGraph { // TODO: Create with capacity 0, maybe offer pre-allocated alternative? fn edge_map(&self, default: T) -> ElementMap { - ElementMap::new(default, |e| e.0 / 2, self.edge_capacity()) + ElementMap::new(default, |e| e.index() / 2, self.edge_capacity()) } fn degree(&self, v: Self::Vertex) -> usize { @@ -180,14 +190,15 @@ impl GraphTopology for AppendGraph { } fn incident_vertices(&self, e: Self::Edge) -> (Self::Vertex, Self::Vertex) { + let ei = e.index(); ( - self.incidences[e.0 ^ 1].adjacent, - self.incidences[e.0].adjacent, + self.incidences[ei ^ 1].adjacent, + self.incidences[ei].adjacent, ) } fn edges(&self) -> impl Iterator { - (0..self.incidences.len()).step_by(2).map(Edge) + (0..self.incidences.len()).step_by(2).map(Edge::new) } fn incident_edges(&self, v: Self::Vertex) -> impl Iterator { @@ -236,7 +247,7 @@ impl GraphTopologyAddition for AppendGraph { fn add_edge(&mut self, v1: Self::Vertex, v2: Self::Vertex) -> Self::Edge { self.add_incidence(v1, v2); self.add_incidence(v2, v1); - Edge(self.incidences.len() - 2) + Edge::new(self.incidences.len() - 2) } } diff --git a/src/models/graph.rs b/src/models/graph.rs index 11a8ff9..c1be295 100644 --- a/src/models/graph.rs +++ b/src/models/graph.rs @@ -1,5 +1,6 @@ //! [`Graph`], an undirected graph topology supporting addition and deletion. +use std::num::NonZeroUsize; use typed_generational_arena::{Arena, Index}; use crate::maps::ElementMap; @@ -31,10 +32,20 @@ pub type GraphIncidence = Incidence; #[derive(Copy, Clone, PartialEq, Eq, Debug)] struct VertexSlot(usize); +// TODO: Benchmark the cost of the index shift. An alternative could be to init Graph::incidences with two dummy entries to avoid 0. #[derive(Copy, Clone, PartialEq, Eq, Debug)] -struct IncidenceSlot(usize); +struct IncidenceSlot(NonZeroUsize); + +impl IncidenceSlot { + fn new(index: usize) -> Self { + IncidenceSlot(NonZeroUsize::new(index + 1).unwrap()) + } + + fn index(self) -> usize { + self.0.get() - 1 + } +} -// TODO: Check if VertexIncidenceHeader and IncidenceEntry can be made smaller. Currently they both take 24 bytes (on 64bit), see https://stackoverflow.com/a/79653173 /// `pub` because [`Vertex`] references it as a type parameter of the underlying arena. Not /// intended for direct external use. #[doc(hidden)] @@ -147,7 +158,7 @@ impl Graph { adjacent: VertexSlot(v2.arr_idx()), }); self.vertices[v1].incidence_count += 1; - self.vertices[v1].first_incidence = Some(IncidenceSlot(edge.arr_idx())); + self.vertices[v1].first_incidence = Some(IncidenceSlot::new(edge.arr_idx())); edge } @@ -186,7 +197,7 @@ impl Graph { let first = vertex_header .first_incidence .expect("incident vertex without incidences, corrupt internal data state"); - if first.0 == e.arr_idx() { + if first.index() == e.arr_idx() { vertex_header.first_incidence = next; } else { let graph: &Graph = self; @@ -195,7 +206,7 @@ impl Graph { .find(|(_, f)| { graph.incidences[*f] .next - .is_some_and(|i| i.0 == e.arr_idx()) + .is_some_and(|i| i.index() == e.arr_idx()) }) .expect("cannot find previous incidence, corrupt internal data state"); self.incidences[previous].next = next; @@ -210,7 +221,7 @@ impl Graph { fn step_incidence(&self, incidence: &mut Option) -> Option<(VertexSlot, Edge)> { // TODO: Benchmark storing full Index (one read, larger entries) vs. slot + get_idx() (two reads, smaller entries). let current = (*incidence)?; - let e = self.incidences.get_idx(current.0).unwrap(); + let e = self.incidences.get_idx(current.index()).unwrap(); let entry = self.incidences[e]; *incidence = entry.next; Some((entry.adjacent, e)) @@ -371,7 +382,7 @@ impl GraphTopologyDeletion for Graph { if e_entry.adjacent != f_entry.adjacent { self.update_incidence_list(e, f_entry.adjacent, e_entry.next, false); self.update_incidence_list(f, e_entry.adjacent, f_entry.next, false); - } else if f_entry.next.is_some_and(|i| e.arr_idx() == i.0) { + } else if f_entry.next.is_some_and(|i| e.arr_idx() == i.index()) { self.update_incidence_list(f, e_entry.adjacent, e_entry.next, true); } else { self.update_incidence_list(e, e_entry.adjacent, f_entry.next, true);