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.
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<T> = ElementMap<Vertex, T>;
@@ -26,13 +43,6 @@ pub type AppendGraphEdgeMap<T> = ElementMap<Edge, T>;
/// An [`Incidence`] for [`AppendGraph`].
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 {
incidence_count: usize,
first_incidence: Option<Edge>,
@@ -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<Item = (Vertex, Edge)> {
@@ -128,7 +138,7 @@ impl AppendGraph {
fn step_incidence(&self, incidence: &mut Option<Edge>) -> 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<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 {
@@ -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<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> {
@@ -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)
}
}
+18 -7
View File
@@ -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<Vertex, Edge>;
#[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<IncidenceSlot>) -> 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);