Back to Home

ART Prefix Trees: Memory and Cache Optimization

Article analyzes problems of standard prefix trees (825 MB memory, 12 cache misses) and optimizations via radix trees and ART (18 MB, 3 misses). Provides benchmarks, node codes and application scenarios for autocomplete and routing.

ART: prefix trees 45 times more compact than standard
Advertisement 728x90

Optimizing Prefix Trees: From 825 MB to 18 MB for Autocomplete

A trie for searching 50,000 commands consumed 825 MB of memory and ran 8 times slower than a hash table (4.8M cycles vs. 600K). Each node was 2,064 bytes with a 256-pointer array, causing frequent cache misses during lookups.

The perf stat benchmark revealed 125K cache misses versus just 18K for the hash table. Searching a 5-character string triggered 5 cache misses because each node didn’t fit in a single cache line.

Node Structure and Memory Usage

Standard node implementation:

Google AdInline article slot
typedef struct trie_node {
    struct trie_node *children[256];  // 2048 bytes
    void *value;                      // 8 bytes
    bool is_end;                      // 1 byte
    // Total: 2064 bytes
} trie_node_t;

For a dataset of 50K commands (average length 8 characters), ~400K nodes were needed—825 MB compared to just 1.2 MB for a hash table, a 687× difference.

Compact Tries: Chain Compression

Solution: Compact tries (Patricia tries), where chains of single-child nodes are compressed into prefixes. Nodes store prefix strings instead of individual characters.

typedef struct radix_node {
    char *prefix;
    int prefix_len;
    struct radix_node *children[256];
    void *value;
} radix_node_t;

Search algorithm:

Google AdInline article slot
void* radix_search(radix_node_t *node, const char *key) {
    while (node) {
        int i = 0;
        while (i < node->prefix_len && key[i] == node->prefix[i]) {
            i++;
        }
        if (i < node->prefix_len) return NULL;
        if (key[i] == '\0') return node->value;
        node = node->children[(unsigned char)key[i]];
        key += i + 1;
    }
    return NULL;
}

Result: Memory dropped to 330 MB, cycles to 2.4M, and cache misses to 6.8—a 2× speedup.

Adaptive Radix Trees (ART)

Problem remained: 98% of the 256-pointer arrays were empty. Solution: adaptive nodes based on child count.

  • Node4 (1–4 children): 40 bytes
  • Node16 (5–16 children): 152 bytes
  • Node48 (17–48 children): 640 bytes
  • Node256 (49+ children): 2048 bytes

Growth strategy: Node4 → Node16 → Node48 → Node256 when limits are exceeded.

Google AdInline article slot

Average node size: 40–152 bytes vs. 2048—memory savings of 10–50×.

Benchmark Results

Comparison across 1M searches over 50K commands:

| Structure | Memory | Cycles | Cache Misses | Speedup |

|-----------|--------|--------|--------------|---------|

| Trie | 825 MB | 4,800 | 12.5 | 1× |

| Radix | 330 MB | 2,400 | 6.8 | 2× |

| ART | 18 MB | 1,200 | 3.2 | 4× |

| Hash | 1.2 MB | 600 | 1.8 | 8× |

ART is 45× more compact than standard trie; Node4/Node16 nodes fit in 1–2 cache lines.

Use Cases for Prefix Trees

Use ART when you need prefix operations:

  • Autocomplete: Traverse subtree in O(k + m), where m is matches. Hash tables require full scan.
  • IP Routing: Longest prefix matching by address bits.
  • Spell Checking: Search via edit distance using prefixes.
  • Sparse Arrays: As used in the Linux kernel (page cache, IDR, XArray).

Avoid for exact lookups, small datasets (<1,000 items), or strings with no shared prefixes.

Compact Tries in the Linux Kernel

The kernel uses radix_tree_node with 64 slots (6 bits per level). For a 32-bit index, that’s 6 levels—~6 cache misses vs. 32 for BST.

Advantages:

  • Range operations support
  • Predictable O(log₆₄ n) complexity
  • Efficient for sparse data

Key Takeaways

  • Standard tries use 687× more memory due to 256-pointer arrays
  • ART reduces memory to 18 MB and cache misses to 3.2 via adaptive nodes
  • Use for prefix queries; hash tables win at exact lookup
  • Compact tries compress chains, delivering 2× speedup
  • Linux kernel uses them for sparse arrays and ID allocation

— Editorial Team

Advertisement 728x90

Read Next