Use non-zero edge indices to reduce size of structs with Option fields

This commit is contained in:
2026-09-03 20:28:41 +02:00
parent c25a8a4fc1
commit 1209d75e15
2 changed files with 44 additions and 22 deletions
+26 -15
View File
@@ -1,5 +1,7 @@
//! [`AppendGraph`], an undirected graph topology supporting addition only. //! [`AppendGraph`], an undirected graph topology supporting addition only.
use std::num::NonZeroUsize;
use crate::maps::ElementMap; use crate::maps::ElementMap;
use crate::traits::{GraphTopology, GraphTopologyAddition, Incidence, IncidenceCursor}; 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)] #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
pub struct Vertex(usize); 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`]. /// An opaque handle identifying an edge in an [`AppendGraph`].
/// ///
/// Handles are stable for the lifetime of the graph. Obtain via graph methods like /// Handles are stable for the lifetime of the graph. Obtain via graph methods like
/// [`AppendGraph::add_edge`] and [`AppendGraph::edges`]. /// [`AppendGraph::add_edge`] and [`AppendGraph::edges`].
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)] #[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. /// An [`ElementMap`] for [`AppendGraph`] vertices.
pub type AppendGraphVertexMap<T> = ElementMap<Vertex, T>; pub type AppendGraphVertexMap<T> = ElementMap<Vertex, T>;
@@ -26,13 +43,6 @@ pub type AppendGraphEdgeMap<T> = ElementMap<Edge, T>;
/// An [`Incidence`] for [`AppendGraph`]. /// An [`Incidence`] for [`AppendGraph`].
pub type AppendGraphIncidence = Incidence<Vertex, Edge>; pub type AppendGraphIncidence = Incidence<Vertex, Edge>;
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 { struct VertexIncidenceHeader {
incidence_count: usize, incidence_count: usize,
first_incidence: Option<Edge>, first_incidence: Option<Edge>,
@@ -118,7 +128,7 @@ impl AppendGraph {
adjacent: v2, adjacent: v2,
}); });
self.vertices[v1.0].incidence_count += 1; 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<Item = (Vertex, Edge)> { fn raw_incidences(&self, v: Vertex) -> impl Iterator<Item = (Vertex, Edge)> {
@@ -128,7 +138,7 @@ impl AppendGraph {
fn step_incidence(&self, incidence: &mut Option<Edge>) -> Option<(Vertex, Edge)> { fn step_incidence(&self, incidence: &mut Option<Edge>) -> Option<(Vertex, Edge)> {
let current = (*incidence)?; let current = (*incidence)?;
let entry = self.incidences[current.0]; let entry = self.incidences[current.index()];
*incidence = entry.next; *incidence = entry.next;
Some((entry.adjacent, current)) Some((entry.adjacent, current))
} }
@@ -160,7 +170,7 @@ impl GraphTopology for AppendGraph {
// TODO: Create with capacity 0, maybe offer pre-allocated alternative? // TODO: Create with capacity 0, maybe offer pre-allocated alternative?
fn edge_map<T: Clone>(&self, default: T) -> ElementMap<Self::Edge, T> { fn edge_map<T: Clone>(&self, default: T) -> ElementMap<Self::Edge, T> {
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 { 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) { fn incident_vertices(&self, e: Self::Edge) -> (Self::Vertex, Self::Vertex) {
let ei = e.index();
( (
self.incidences[e.0 ^ 1].adjacent, self.incidences[ei ^ 1].adjacent,
self.incidences[e.0].adjacent, self.incidences[ei].adjacent,
) )
} }
fn edges(&self) -> impl Iterator<Item = Self::Edge> { fn edges(&self) -> impl Iterator<Item = Self::Edge> {
(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<Item = Self::Edge> { fn incident_edges(&self, v: Self::Vertex) -> impl Iterator<Item = Self::Edge> {
@@ -236,7 +247,7 @@ impl GraphTopologyAddition for AppendGraph {
fn add_edge(&mut self, v1: Self::Vertex, v2: Self::Vertex) -> Self::Edge { fn add_edge(&mut self, v1: Self::Vertex, v2: Self::Vertex) -> Self::Edge {
self.add_incidence(v1, v2); self.add_incidence(v1, v2);
self.add_incidence(v2, v1); self.add_incidence(v2, v1);
Edge(self.incidences.len() - 2) Edge::new(self.incidences.len() - 2)
} }
} }
+18 -7
View File
@@ -1,5 +1,6 @@
//! [`Graph`], an undirected graph topology supporting addition and deletion. //! [`Graph`], an undirected graph topology supporting addition and deletion.
use std::num::NonZeroUsize;
use typed_generational_arena::{Arena, Index}; use typed_generational_arena::{Arena, Index};
use crate::maps::ElementMap; use crate::maps::ElementMap;
@@ -31,10 +32,20 @@ pub type GraphIncidence = Incidence<Vertex, Edge>;
#[derive(Copy, Clone, PartialEq, Eq, Debug)] #[derive(Copy, Clone, PartialEq, Eq, Debug)]
struct VertexSlot(usize); 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)] #[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 /// `pub` because [`Vertex`] references it as a type parameter of the underlying arena. Not
/// intended for direct external use. /// intended for direct external use.
#[doc(hidden)] #[doc(hidden)]
@@ -147,7 +158,7 @@ impl Graph {
adjacent: VertexSlot(v2.arr_idx()), adjacent: VertexSlot(v2.arr_idx()),
}); });
self.vertices[v1].incidence_count += 1; 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 edge
} }
@@ -186,7 +197,7 @@ impl Graph {
let first = vertex_header let first = vertex_header
.first_incidence .first_incidence
.expect("incident vertex without incidences, corrupt internal data state"); .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; vertex_header.first_incidence = next;
} else { } else {
let graph: &Graph = self; let graph: &Graph = self;
@@ -195,7 +206,7 @@ impl Graph {
.find(|(_, f)| { .find(|(_, f)| {
graph.incidences[*f] graph.incidences[*f]
.next .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"); .expect("cannot find previous incidence, corrupt internal data state");
self.incidences[previous].next = next; self.incidences[previous].next = next;
@@ -210,7 +221,7 @@ impl Graph {
fn step_incidence(&self, incidence: &mut Option<IncidenceSlot>) -> Option<(VertexSlot, Edge)> { fn step_incidence(&self, incidence: &mut Option<IncidenceSlot>) -> Option<(VertexSlot, Edge)> {
// TODO: Benchmark storing full Index (one read, larger entries) vs. slot + get_idx() (two reads, smaller entries). // TODO: Benchmark storing full Index (one read, larger entries) vs. slot + get_idx() (two reads, smaller entries).
let current = (*incidence)?; 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]; let entry = self.incidences[e];
*incidence = entry.next; *incidence = entry.next;
Some((entry.adjacent, e)) Some((entry.adjacent, e))
@@ -371,7 +382,7 @@ impl GraphTopologyDeletion for Graph {
if e_entry.adjacent != f_entry.adjacent { if e_entry.adjacent != f_entry.adjacent {
self.update_incidence_list(e, f_entry.adjacent, e_entry.next, false); self.update_incidence_list(e, f_entry.adjacent, e_entry.next, false);
self.update_incidence_list(f, e_entry.adjacent, f_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); self.update_incidence_list(f, e_entry.adjacent, e_entry.next, true);
} else { } else {
self.update_incidence_list(e, e_entry.adjacent, f_entry.next, true); self.update_incidence_list(e, e_entry.adjacent, f_entry.next, true);