Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Solutions: 23 — Index maps

Exercise 1 — Build the map

#![allow(unused)]
fn main() {
const INVALID: u32 = u32::MAX;

struct World {
    creatures: Vec<CreatureRow>,
    id_to_slot: Vec<u32>, // length = high-water mark of ids ever issued
    next_id: u32,
}

fn append(world: &mut World, mut row: CreatureRow) -> u32 {
    let id = world.next_id;
    world.next_id += 1;
    while world.id_to_slot.len() <= id as usize {
        world.id_to_slot.push(INVALID);
    }
    row.id = id;
    let slot = world.creatures.len() as u32;
    world.creatures.push(row);
    world.id_to_slot[id as usize] = slot;
    id
}
}

The map grows lazily as new ids are issued. INVALID marks dead/never-used slots.

Exercise 2 — O(1) presence query

#![allow(unused)]
fn main() {
fn is_alive(world: &World, id: u32) -> bool {
    (id as usize) < world.id_to_slot.len()
        && world.id_to_slot[id as usize] != INVALID
}

fn slot_of(world: &World, id: u32) -> Option<usize> {
    let slot = *world.id_to_slot.get(id as usize)?;
    if slot == INVALID { None } else { Some(slot as usize) }
}
}

is_alive is two array reads and a comparison — a handful of nanoseconds. Compare with the linear scan from §17, which is hundreds of microseconds at 1 M creatures.

Exercise 3 — Maintain on swap_remove

#![allow(unused)]
fn main() {
fn delete_by_id(world: &mut World, id: u32) {
    let slot = world.id_to_slot[id as usize] as usize;
    world.creatures.swap_remove(slot);
    if slot < world.creatures.len() {
        // Some row was moved into `slot`. Update its mapping.
        let moved_id = world.creatures[slot].id;
        world.id_to_slot[moved_id as usize] = slot as u32;
    }
    world.id_to_slot[id as usize] = INVALID;
}
}

Three writes: remove from creatures, update the moved row’s slot, mark the deleted id invalid. ~12 bytes per delete; ~12 GB/s of memory bandwidth means each delete is well under 10 ns of bandwidth cost.

Exercise 4 — Time the difference

At 1 M creatures, the linear-scan presence check costs ~1 ms. The indexed version costs ~50 ns. Run 100 000 queries per tick:

  • Linear: 100 000 × 1 ms = 100 seconds. Impossible.
  • Indexed: 100 000 × 50 ns = 5 ms. Fits 30 Hz with margin.

The factor of N (a million) shows up in real wall time.

Exercise 5 — Bandwidth cost of cleanup

1 000 deletes per tick × 12 bytes each = 12 KB written per tick. At ~12 GB/s memory bandwidth, that is ~1 µs. Compare to a 30 Hz budget of 33 ms: ~0.003 % of the tick. The cleanup pass is essentially free; the system can afford to run every tick without measurable cost.

Exercise 6 — Sort-for-locality compatibility

#![allow(unused)]
fn main() {
fn sort_creatures_for_locality(world: &mut World) {
    let mut order: Vec<usize> = (0..world.creatures.len()).collect();
    order.sort_by_key(|&i| spatial_cell(world.creatures[i].pos));

    // Apply the permutation to creatures.
    let new_creatures: Vec<_> = order.iter().map(|&i| world.creatures[i].clone()).collect();
    world.creatures = new_creatures;

    // Rewrite id_to_slot.
    for (new_slot, row) in world.creatures.iter().enumerate() {
        world.id_to_slot[row.id as usize] = new_slot as u32;
    }
}
}

Every slot moves; the map is rewritten entirely. External references to ids continue to work; references to slots would not (which is why nobody holds slots — they hold ids).

Exercise 7 — From-scratch generational arena

#![allow(unused)]
fn main() {
struct SlotMap<T> {
    items: Vec<T>,
    gen:   Vec<u32>,
    free:  Vec<u32>,
}

impl<T: Clone + Default> SlotMap<T> {
    fn insert(&mut self, t: T) -> (u32, u32) {
        if let Some(slot) = self.free.pop() {
            self.items[slot as usize] = t;
            (slot, self.gen[slot as usize])
        } else {
            let slot = self.items.len() as u32;
            self.items.push(t);
            self.gen.push(0);
            (slot, 0)
        }
    }

    fn remove(&mut self, slot: u32) {
        self.gen[slot as usize] += 1;
        self.free.push(slot);
        self.items[slot as usize] = Default::default(); // optional
    }

    fn get(&self, slot: u32, gen: u32) -> Option<&T> {
        if self.gen[slot as usize] == gen { Some(&self.items[slot as usize]) } else { None }
    }
}
}

Compare with slotmap::SlotMap — the same machinery. The crate adds a packed key (slot + gen in one u64), an iterator API, and a null() sentinel. The shape is identical.