Powrót do strony głównej

Drzewa prefiksowe ART: optymalizacja pamięci i pamięci podręcznej

Artykuł analizuje problemy standardowych drzew prefiksowych (825 MB pamięci, 12 cache misses) i optymalizacje za pomocą drzew radixowych i ART (18 MB, 3 misses). Przedstawiono benchmarki, kody węzłów i scenariusze zastosowań dla autozakończenia i routingu.

ART: drzewa prefiksowe 45 razy kompaktniejsze niż standardowe
Advertisement 728x90

Optymalizacja drzew prefiksowych: od 825 MB do 18 MB dla uzupełniania automatycznego

Drzewo prefiksowe do wyszukiwania 50 tys. poleceń zajmowało 825 MB pamięci i działało 8 razy wolniej niż tablica mieszająca (4,8 mln cykli w porównaniu do 600 tys.). Każdy węzeł miał 2064 bajty z tablicą 256 wskaźników, co powodowało wiele przestrajek bufora podczas wyszukiwania.

Benchmark perf stat wykazał 125 tys. przestrajek bufora w porównaniu do 18 tys. w przypadku tablicy mieszającej. Wyszukiwanie ciągu o długości 5 znaków generuje 5 przestrajek, ponieważ żaden węzeł nie mieści się w linii bufora.

Struktura węzła i zużycie pamięci

Typowa implementacja węzła:

Google AdInline article slot
typedef struct trie_node {
    struct trie_node *children[256];  // 2048 bajtów
    void *value;                      // 8 bajtów
    bool is_end;                      // 1 bajt
    // Razem: 2064 bajty
} trie_node_t;

Dla zbioru danych z 50 tys. poleceń (średnia długość 8 znaków) potrzeba ok. 400 tys. węzłów. Łącznie 825 MB w porównaniu do 1,2 MB w tablicy mieszającej — różnica 687-krotna.

Drzewa bazowe: kompresja łańcuchów

Rozwiązanie to drzewa bazowe (Patricia tries), gdzie łańcuchy pojedynczych węzłów są zwężane do prefiksów. Węzeł przechowuje ciąg prefiksu zamiast poszczególnych znaków.

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

Algorytm wyszukiwania:

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;
}

Wynik: pamięć spadła do 330 MB, cykle do 2,4 mln, przestrajki bufora do 6,8. Prędkość wzrosła dwukrotnie.

Adaptacyjne drzewa bazowe (ART)

Problem pozostał: tablice 256 wskaźników są wypełnione tylko na 2%. Rozwiązanie — adaptacyjne węzły według liczby dzieci.

  • Node4 (1–4 dzieci): 40 bajtów
  • Node16 (5–16 dzieci): 152 bajty
  • Node48 (17–48 dzieci): 640 bajtów
  • Node256 (49+ dzieci): 2048 bajtów

Strategia rozwoju: Node4 → Node16 → Node48 → Node256 przy przekroczeniu limitu.

Google AdInline article slot

Średni węzeł: 40–152 bajtów zamiast 2048. Oszczędność pamięci 10–50 razy.

Benchmarki optymalizacji

Porównanie przy 1 mln wyszukiwań w 50 tys. poleceń:

| Struktura | Pamięć | Cykle | Przestrajki bufora | Przyspieszenie |

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

| 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 jest 45 razy bardziej kompaktowe niż standardowe drzewo trie, a węzły Node4/Node16 mieszczą się w 1–2 liniach bufora.

Scenariusze zastosowania drzew prefiksowych

Użyj ART, gdy potrzebujesz operacji prefiksowych:

  • Uzupełnianie automatyczne: przeglądanie poddrzewa w czasie O(k + m), gdzie m to liczba pasujących wyników. Tablica mieszająca wymaga pełnego skanowania.
  • Trasowanie IP: dopasowanie najdłuższego prefiksu według bitów adresu.
  • Sprawdzanie pisowni: wyszukiwanie według odległości edytorskiej przez prefiksy.
  • Tablice rozrzedzone: jak w jądrze Linux (bufor stron, IDR, XArray).

Unikaj dla dokładnego wyszukiwania, małych zbiorów danych (<1000 elementów) lub ciągów bez wspólnych prefiksów.

Drzewa bazowe w jądrze Linux

Jądro używa radix_tree_node z 64 slotami (6 bitów poziomu). Dla indeksu 32-bitowego – 6 poziomów, ~6 przestrajek bufora w porównaniu do 32 w BST.

Zalety:

  • Operacje na zakresach
  • Przewidywalna złożoność O(log₆₄ n)
  • Skuteczne dla danych rozrzedzonych

Co ważne

  • Standardowe drzewa trie zużywają 687× więcej pamięci ze względu na tablice 256 wskaźników
  • ART zmniejsza pamięć do 18 MB i przestrajki bufora do 3,2 dzięki adaptacyjnym węzłom
  • Używaj dla zapytań prefiksowych; tablice mieszające wygrywają w dokładnym wyszukiwaniu
  • Drzewa bazowe zwężają łańcuchy, dając 2-krotne przyspieszenie
  • W jądrze Linux — standard dla tablic rozrzedzonych i dystrybucji ID

— Editorial Team

Advertisement 728x90

Czytaj dalej