prefix-trie 0.9.0

Prefix trie (tree) datastructure (both a set and a map) that provides exact and longest-prefix matches.
Documentation

CI test codecov version downloads docs.rs license Crates.io MSRV

Prefix-Trie

This crate provides prefix-map and prefix-set collections for IP prefixes and other fixed-width prefix types. PrefixMap is backed by a compact TreeBitMap-style trie and supports exact, longest-prefix, and shortest-prefix matches. The crate supports both IPv4 and IPv6 (from either ipnet, ipnetwork, or cidr). It also supports any tuple (R, u8), where R is any unsigned primitive integer (u8, u16, u32, u64, u128, or usize).

Prefixes are not stored verbatim. They are reconstructed from their trie position when returned from map and set operations, so host bits outside the prefix length are not preserved.

This crate also provides a JointPrefixMap and JointPrefixSet that contains two tables, one for IPv4 and one for IPv6.

Migrate to version 0.9

Releases up to 0.8.4 used a binary tree representation. With version 0.9, this crate migrated to a TreeBitMap, described by W. Eatherton, Z. Dittia, G. Varghes. This yields significant performance improvements (~8x faster inserts & lookups for all IPv4 addresses in internet routing tables) and memory reduction (~4x less).

However, this also comes with significant changes in the interface:

  • Prefixes are reconstructed from the tree location and no longer stored. Consequently, information in the host part of an address is no longer maintained. Additionally, the function yields prefixes by value instead of by reference. If you wish to store the host-bits as well, consider constructing a type PrefixMap<P, (P, T)>.
  • Mutable views of the tree can no longer modify the tree structure (to maintain memory safety). This is because one can construct two mutable views pointing to branches within the same multi-bit node.
  • Thanks to bitmaps, set operations can now be chained (as we can define a common interface for them to return their bitmaps).

Comparison with related projects

The comparison baseline is the treebitmap crate, ip_network_table-deps-treebitmap, which provides an IP lookup table similar to PrefixMap.

The following experiments perform 100_000 iterations and report the time for the complete batch. The random-prefix experiment uses randomly generated IPv4 prefixes. The BGP-prefix experiment uses all IPv4 prefixes exchanged at AMS-IX in the bundled snapshot, currently 1,058,306 prefixes. See benches/benchmark.rs for more details.

Operation Experiment PrefixMap TreeBitMap ratio
Lookup random prefixes 3.54ms 6.38ms ~1.8x
Insert & Remove random prefixes 9.52ms 15.59ms ~1.6x
Lookup AMS-IX prefixes 6.48ms 11.11ms ~1.7x
Insert & Remove AMS-IX prefixes 12.18ms 17.34ms ~1.4x

The ratio is TreeBitMap divided by PrefixMap, so values above 1 mean PrefixMap was faster in that run.

The memory benchmark (cargo test --bench memory --release -- --nocapture) stores all 1,058,306 AMS-IX IPv4 prefixes with 32-bit values. Both PrefixMap and TreeBitMap report 12.0 mB for that map.

In addition, prefix-trie includes a PrefixSet analogous to std::collections::HashSet. Set operations are exposed through composable trie views, so operations such as union, intersection, difference, covering union, and covering difference can be combined without building temporary maps. prefix-trie has an interface similar to std::collections, and its longest-prefix matching is not limited to individual host addresses.

Description of the Tree

PrefixMap stores the logical binary prefix trie in multi-bit nodes. Each internal node covers five consecutive binary-trie levels. A node at depth d can hold values for prefixes with lengths d..=d+4, and it has up to 32 child slots for subtries rooted at depth d+5.

Each node stores two bitmaps: one for the value slots that are present in the node, and one for the child slots that are present below it. The allocators store multi-bit nodes and value cells in compact, linearized arrays, which improves cache locality and keeps lookup and traversal decisions local to a node. Physical slots are derived from the bitmaps with a popcount, avoiding one pointer per possible branch.

A stored entry is identified by its path through the trie and by a value bit inside the final multi-bit node. The prefix object passed to insert is not stored alongside the value. Returned prefixes are therefore reconstructed and canonicalized to the prefix length.

Traversals

Iterators traverse the logical prefix trie in lexicographic order and yield reconstructed owned prefixes together with references or owned values. Complete iteration is linear in the number of stored entries and trie nodes visited.

Set operations use the same view infrastructure. union, intersection, difference, covering_union, and covering_difference traverse the involved trie views together and yield results in lexicographic order. Covering variants also report longest-prefix matches from the opposite side where appropriate.

Trie Views

TrieView is a trait for immutable, mutable, and composed cursors into a trie. Concrete leaf views are TrieRef, created from &PrefixMap or &PrefixSet, and TrieRefMut, created from mutable references. Both are obtained through the AsView trait: call map.view() for a full-trie view or map.view_at(&prefix) for a non-empty subtrie.

Views can be rooted at a prefix even when no value is stored exactly at that prefix. If the prefix falls inside an existing multi-bit node, the view masks that node's value and child bitmaps so that iteration and search stay inside the requested subtrie. Composed views such as UnionView, IntersectionView, and DifferenceView also implement TrieView, so view operations can be chained before iterating.

Operations on the Tree

Most point operations are bounded by prefix width, not by the number of stored entries. Let w be the number of bits in the prefix representation, and let h = ceil((w + 1) / 5) be the maximum number of multi-bit nodes on a search path. For IPv4, h <= 7; for IPv6, h <= 26. Let n be the number of stored entries, and let v be the number of trie nodes visited by a traversal.

Operation Complexity
len, is_empty, mem_size O(1)
get, get_mut, contains_key O(h)
get_lpm, get_spm, cover O(h)
entry, insert O(h)
remove, remove_keep_tree O(h)
children, view_at O(h) to create, then linear in the subtrie
iter, keys, values O(n + v) for a complete traversal
retain, clear O(n + v)
remove_children O(h + m) where m is the removed subtrie size
union, intersection, difference, ... linear in the trie portions visited
Operations on an occupied map::Entry O(1) after the entry lookup
Inserting through a vacant map::Entry O(h) worst case

There are three removal styles:

  • PrefixMap::remove will remove an entry from the tree and modify the tree structure as if the value was never inserted before. It may remove now-empty multi-bit nodes and compact their allocator blocks.
  • PrefixMap::remove_children will remove all entries that are contained within the given prefix, including entries stored in the same multi-bit node and in child nodes below it.
  • PrefixMap::remove_keep_tree removes only the value and leaves the existing node structure in place, which can make reinserting the same prefix cheaper but may leave empty internal nodes for future traversals to pass through.