2009-08-30 19 views
14

Mam problem: Potrzebuję przestrzenno-sprawnego wyszukiwania danych systemu plików na podstawie prefiksu ścieżki pliku. Przedrostek wyszukiwania posortowanego tekstu, innymi słowy. Użyj tria, mówisz, i pomyślałem to samo. Problem polega na tym, że próby nie są wystarczająco wydajne, nie bez innych sztuczek.Przestrzeń w pamięci struktura dla posortowanego tekstu wspierającego wyszukiwanie prefiksów

mam sporo danych:

  • około 450m w zwykły tekst Unix formatu notowań na dysku
  • około 8 milionów wierszy
  • domyślny gzip kompresuje do 31m
  • bzip2 domyślnie kompresuje do 21M

Nie chcę jeść prawie w pobliżu 450M w pamięci. W tym momencie chciałbym być szczęśliwy, używając około 100M, ponieważ jest dużo redundancji w postaci prefiksów.

Używam C# dla tego zadania, a prosta implementacja trie nadal będzie wymagać jednego węzła liści dla każdej linii w pliku. Biorąc pod uwagę, że każdy węzeł liści będzie wymagał jakiegoś odniesienia do końcowego fragmentu tekstu (32 bity, na przykład indeks do tablicy danych łańcuchowych, aby zminimalizować duplikowanie ciągów), a narzut obiektu CLR to 8 bajtów (zweryfikowano przy użyciu windbg/SOS) , Będę wydawać> 96 000 000 bajtów na narzucie strukturalnym bez przechowywania tekstu.

Przyjrzyjmy się niektórym statystycznym atrybutom danych. Gdy nadziewane w trie:

  • całkowite niepowtarzalne „porcjach” tekstu około 1,1 miliona
  • całkowite unikalne fragmentów od około 16M na dysku w postaci pliku tekstowego
  • średnia długość fragmentu wynosi 5,5 znaków, max 136
  • przy braku duplikatów, łącznie około 52 milionów znaków w kawałkach
  • Wewnętrzne węzły Trie średnio około 6,5 dzieci z max. 44
  • około 1,8M węzłów wewnętrznych.

przekroczenie szybkości tworzenia liści wynosi około 15%, nadmiar tworzenie węzeł wewnętrzny wynosi 22% - przez nadmiar stworzenia, to znaczy liści i węzły wewnętrzne powstałe w trakcie budowy trie, ale nie w końcowym trie jako proporcja ostateczna liczba węzłów każdego typu.

Oto analiza sterty od SOS, wskazując, gdzie jest przyzwyczaić najbardziej pamięć:

[MT ]--[Count]----[ Size]-[Class           ] 
03563150  11   1584 System.Collections.Hashtable+bucket[] 
03561630  24   4636 System.Char[] 
03563470  8   6000 System.Byte[] 
00193558  425  74788  Free 
00984ac8 14457  462624 MiniList`1+<GetEnumerator>d__0[[StringTrie+Node]] 
03562b9c  6  11573372 System.Int32[] 
*009835a0 1456066  23297056 StringTrie+InteriorNode 
035576dc  1  46292000 Dictionary`2+Entry[[String],[Int32]][] 
*035341d0 1456085  69730164 System.Object[] 
*03560a00 1747257  80435032 System.String 
*00983a54 8052746  96632952 StringTrie+LeafNode 

Dictionary<string,int> jest używany do mapowania fragmentów ciągów do indeksów w List<string> i można je wyrzucić po zakończeniu budowy trie, chociaż GC nie wydaje się go usuwać (kilka wyraźnych kolekcji zostało zrobionych przed tym zrzutem) - !gcroot w SOS nie wskazuje żadnych korzeni, ale przewiduję, że późniejszy GC go uwolni.

MiniList<T> zastępuje List<T> stosując dokładnie rozmiarze (to wzrost liniowy O(n^2) wykonania dodatkowo) T[] uniknąć straty przestrzeni; jest to typ wartości i jest używany przez InteriorNode do śledzenia dzieci.Ten T[] został dodany do kupki System.Object[].

Tak więc, jeśli podliczę "interesujące" elementy (oznaczone *), otrzymuję około 270M, co jest lepsze niż surowy tekst na dysku, ale nadal nie jest wystarczająco zbliżony do mojego celu. Pomyślałem, że obiekt .NET nad głową było zbyt dużo, i stworzył nową „Slim” Trie, używając tylko tablice wartość typu do przechowywania danych:

class SlimTrie 
{ 
    byte[] _stringData; // UTF8-encoded, 7-bit-encoded-length prefixed string data 

    // indexed by _interiorChildIndex[n].._interiorChildIndex[n]+_interiorChildCount[n] 
    // Indexes interior_node_index if negative (bitwise complement), 
    // leaf_node_group if positive. 
    int[] _interiorChildren; 

    // The interior_node_index group - all arrays use same index. 
    byte[] _interiorChildCount; 
    int[] _interiorChildIndex; // indexes _interiorChildren 
    int[] _interiorChunk; // indexes _stringData 

    // The leaf_node_index group. 
    int[] _leafNodes; // indexes _stringData 

    // ... 
} 

Struktura ta przyniosła dół ilość danych do 139m, a wciąż jest wydajnym traserem do operacji tylko do odczytu. A ponieważ jest to tak proste, mogę trywialnie zapisać je na dysku i przywrócić je, aby za każdym razem uniknąć kosztu ponownego utworzenia gry.

Jakie są więc sugestie dotyczące bardziej wydajnych struktur wyszukiwania prefiksów niż trie? Alternatywne podejścia, które powinienem rozważyć?

+0

Jakiego rodzaju użyjesz danych? Dużo przetwarzania lub tylko kilka wyszukiwań; Czy możesz dać wyobrażenie o tym, jaki kompromis pomiędzy wydajnym przechowywaniem a przetwarzaniem jest akceptowalny? – Jackson

+0

Zasadniczo polega to na buforowaniu operacji wyszukiwania plików systemu, tak aby nie trzeba było sprawdzać fizycznego dysku w przypadku takich rzeczy jak pobieranie wszystkich plików do katalogu, wszystkie pliki rekurencyjnie w katalogu itp. Bez konsultacji z dyskiem, który nieodmiennie nie jest w pamięci i jest w rzeczywistości w sieci => zdecydowanie za dużo w obie strony. Oczekiwano, że wykonanie 150 prefiksów prefiksów (czyli znalezienie wszystkich linii z tym prefiksem) zwracających średnio 100 linii nie powinno zająć więcej niż, powiedzmy, 100ms. Obecnie podejście 'SlimTrie' zajmuje 10 sekund, aby załadować z dysku i wyświetlić 8 000 000 linii => ~ 18 ms. –

+0

A to z wyłączoną optymalizacją, z włączonym, 8,5 sekundy - włączając w to uruchamianie aplikacji. 140M nie jest tak źle, ale biorąc pod uwagę nadmiarowość w tych danych, jestem pewien, że można go poprawić. –

Odpowiedz

2

Ponieważ istnieje tylko 1,1 miliona porcji, można indeksować porcję przy użyciu 24 bitów zamiast 32 bitów i zaoszczędzić tam miejsca.

Można również skompresować porcje. Być może Huffman coding jest dobrym wyborem. Spróbowałbym również następującą strategię: zamiast używać znaku jako symbolu do kodowania, powinieneś kodować przejścia postaci. Więc zamiast patrzeć na prawdopodobieństwo pojawienia się postaci, spójrz na prawdopodobieństwo przejścia w Markov chain, gdzie stan jest bieżącą postacią.

+0

Drzewo Huffmana to pierwsza rzecz, którą napisałem po tym, jak zobaczyłem kawałki w trie - myślałem o próbach zakodowania linii jako ciągi bitów, jeden ciąg dla każdego kawałka, połączony - ale podczas gdy pisałem logikę bitowego pakowania, Zastanawiałem się, czy zamiast tego używać tablic o płaskich wartościach do kodowania trie. Wdrożenie kodowania Huffmana poprawnie i wydajnie, a zwłaszcza dekodowanie, dość szybko staje się dość nudne. Mogę go odebrać i być może kodować w oparciu o częstotliwość znaków. –

+0

Tak, indeksowanie przy użyciu mniejszej liczby bitów niż 32 jest czymś, o czym myślałem. Inne rzeczy: 16-znakowe dane znakowe kończą się na 24 bity, ale jeśli wyrównałem dane znakowe do granic wyrazów, kosztując średnio 0,5 bajta na porcję, mógłbym użyć 24 bitów do indeksowania do 32M pozycji, dla połowy oszczędności. I ta logika bitowego pakowania, którą pisałem dla kodowania drzewa Huffmana, może przydać się przy używaniu mniejszej liczby bajtów do przechowywania indeksów. Moim następnym krokiem będzie prawdopodobnie napisanie klasy "tablicy bitowej". –

+0

Przyznam tej wygranej. Napisałem upakowaną tablicę klasy, która może indeksować podpisane lub niepodpisane liczby całkowite o stałej szerokości bitowej i określić maksymalną szerokość wymaganą przy konwersji z mojego ciągłego StringTrie ładującego czas ładowania na moją niezmienną SlimTrie. Przechowywanie SlimTrie na dysku i ponowne ładowanie w późniejszym czasie oszczędza czas i pamięć, unikając nieczystości śmieci GC. Teraz do 75M! –

0

Idea poza ścianą: Zamiast stołu z hashami. Miałbyś w pamięci tylko hash i dane łańcucha, być może skompresowane.

Czy możesz pozwolić sobie na przeczytanie jednej strony? Tylko hash i pozycja pliku w pamięci, pobierają "stronę" z liniami pasującymi do tego hasha, przypuszczalnie małą liczbą uporządkowanych linii, stąd bardzo szybkie wyszukiwanie w razie kolizji.

+0

Robienie 150 stara się przeczytać 100 linii z każdej lokalizacji nie jest tak szybkie, jak by sobie życzyć - tak właśnie robiłem, zanim podjąłem podejście trie. Używałem indeksu liniowego w pliku tekstowym, tj.plik zawierający zasadniczo płaską tablicę przesunięć 32-bitowych na początku każdej linii, z plikiem w porządku posortowanym. Losowy szuka ponad 450M pliku zabije cię. –

+0

Dla pomysłu tabeli mieszania - nie całkiem cię rozumiem. Wyszukiwanie przedrostkowe nie jest kluczem o ustalonej długości, może to być a/b, a/b/c, a/b/c/d, itd. W pierwszym trie tworzę - nie szczupły - już jestem przechowywanie danych postaci raz za pomocą indeksów. –

+0

Pomysł polegał na tym, aby zaszyfrować cały prefiks, bez względu na to, jak długo. Spowoduje to, że liczba, która jest indeksem "strony", strona zawiera wszystkie wiersze pasujące do tego skrótu. Dlatego robisz tylko jeden logiczny odczyt, wracając do niektórych linii. [To może być kilka fizycznych odczytów, ale mam nadzieję, że mniej niż 150 poszukiwań.] Po prostu odrzucasz wszystkie mieszania, których nie chcesz. – djna

1

Możesz znaleźć artykuł naukowy związany z twoim problemem here (cytat z autorów: "Eksperymenty pokazują, że nasz indeks obsługuje szybkie zapytania w obrębie zajmowanej przestrzeni, które są zbliżone do uzyskiwanych przez kompresję słownika napisanego przez gzip, bzip lub ppmdi. "- ale niestety papier jest tylko płatnością). Nie jestem pewien, jak trudne są te pomysły do ​​wdrożenia. Autorzy tego artykułu mają website, gdzie można znaleźć również implementacje (pod "Index Collection") różnych skompresowanych algorytmów indeksowych .

Jeśli chcesz kontynuować swoje podejście, sprawdź witryny o numerach Crit-bit trees i Radix tree.

+0

Oczyszczona odpowiedź osoby, której zażąda (mógł tylko link do jednej strony) –

+0

Jest OK, mam abonament ACM. Zajrzę w to. –

+0

W rzeczywistości, drzewo radix, lub Patricia trie, jest sposobem przechowywania danych z mojego trieta - tylko przechowywanie pojedynczego znaku na krawędzi/węźle byłoby wyraźnie szalone dla orientacji w przestrzeni. –

Powiązane problemy