Verwenden von SIMD/AVX/SSE für das Traversieren von Bäumen

Ich untersuche gerade, ob es möglich wäre, einen Van Emde Boas (oder irgendeinen Baum) Tree Traversal zu beschleunigen. Bei einer einzigen Suchanfrage als Eingabe, die bereits mehrere Baumknoten in der Cache-Zeile hat (van emde Boas-Layout), scheint die Baumdurchquerung ein Instruktions-Engpass zu sein.

Da ich etwas neu in SIMD/AVX/SSE-Anweisungen bin, möchte ich von Experten in diesem Thema wissen, ob es möglich wäre, mehrere Knoten gleichzeitig mit einem Wert zu vergleichen und dann herauszufinden, welcher Baumpfad weiter verfolgt werden soll. Meine Recherche führte zu folgender Frage:

Wieviele CPU-Zyklen/Anweisungen werden bei der Erstellung des SIMD/AVX/SSE-Registers usw. verschwendet. Dies würde für den Weg nützlich sein, wenn die Konstruktion mehr Zeit benötigt, als den gesamten Subbaum manuell zu durchlaufen (2 + 4 + 8 Knoten in 1 Cache-Line der Größe 64 Bytes).

Wie viele CPU-Zyklen/Anweisungen werden verschwendet, um das richtige SIMD/AVX/SSE-Register zu finden, das die Antwort darauf enthält, welchem ​​Pfad gefolgt werden soll? Könnte jemand auf eine kluge Weise kommen, so dass diese "findMinimumInteger" AVX Anweisungen verwendet werden könnten, um zu entscheiden, dass in 1 (??) CPU-Zyklus?

Was ist deine Vermutung?

Another, more tricky approach to speed up tree traversal would be to have multiple search querys run down at once, when there is high probability to land in nodes closely together in the last tree level. Any guesses on this ? Ofc it would have to put those querys aside that do not belong to the same sub-tree any longer and then recursively find them after finishing the first "parallel traversal" of the tree.. The tree querys have sequential, though not constant access patterns (query[i] always < than query[i+1]).

Wichtig: es handelt sich um Integer Tree's, weshalb van Emde Boas Tree verwendet wird (vielleicht versucht x-fast/y-fast später)

Ich bin neugierig, wie hoch die 50 Cent sind, wenn man bedenkt, dass man an der höchsten erreichbaren Leistung bei großen Bäumen interessiert sein könnte. Vielen Dank im Voraus für die Zeit, die Sie dafür ausgeben :-)

0
Wir werden sowieso massive Gewinde verwenden. Dies ist nur die effizienteste Implementierung eines einzelnen Baums auf AVX512-Hardware.
hinzugefügt der Autor user1610743, Quelle
Wenn Sie viele Bäume haben, wäre ich versucht, jede Baumsuche zu einem parallelen Thema zu machen. (Wir machen das in Programmanalyse/Transformationstool, das wir bauen; scheint vernünftig zu funktionieren). Warum ist das nicht eine Ihrer Optionen? Eine weitere Idee: Wenn Sie mehrere Abfragen haben und Sie wissen, was sie im Voraus sind, können Sie sie zu einem FSA zusammenstellen, der die Suchvorgänge steuert. Der Teil der FSA, der von allgemeinen Abfrage-Unterbegriffen erzeugt wird, wird nur einmal mit beträchtlichen Einsparungen verarbeitet. (Siehe LR-Parser für einen ähnlichen Muster-Produ
hinzugefügt der Autor Ira Baxter, Quelle

2 Antworten

Basierend auf Ihrem Code habe ich 3 Optionen evaluiert: AVX2-powered, nested branching (4 Jumps) und eine Branchless-Variante. Dies sind die Ergebnisse:

// Leistungstabelle ... // Alle mit Cachelinie Größe 64byteAligned Chunks (van Emde-Boas Layout); Schleife pro Cacheline abgerollt; // Alle Optimierungen sind eingeschaltet. Jedes Element ist 4 Byte lang. Intel i7 4770k Haswell @ 3,50 GHz

Type        ElementAmount       LoopCount       Avg. Cycles/Query
===================================================================
AVX2        210485750           100000000       610 cycles    
AVX2        21048575            100000000       427 cycles           
AVX2        2104857             100000000       288 cycles 
AVX2        210485              100000000       157 cycles   
AVX2        21048               100000000       95 cycles  
AVX2        2104                100000000       49 cycles    
AVX2        210                 100000000       17 cycles 
AVX2        100                 100000000       16 cycles   


Type        ElementAmount       LoopCount       Avg. Cycles/Query
===================================================================  
Branching   210485750           100000000       819 cycles 
Branching   21048575            100000000       594 cycles 
Branching   2104857             100000000       358 cycles 
Branching   210485              100000000       165 cycles 
Branching   21048               100000000       82 cycles
Branching   2104                100000000       49 cycles 
Branching   210                 100000000       21 cycles 
Branching   100                 100000000       16 cycles   


Type        ElementAmount       LoopCount       Avg. Cycles/Query
=================================================================== 
BranchLESS  210485750           100000000       675 cycles 
BranchLESS  21048575            100000000       602 cycles 
BranchLESS  2104857             100000000       417 cycles
BranchLESS  210485              100000000       273 cycles 
BranchLESS  21048               100000000       130 cycles 
BranchLESS  2104                100000000       72 cycles 
BranchLESS  210                 100000000       27 cycles 
BranchLESS  100                 100000000       18 cycles

Also meine Schlussfolgerung sieht so aus: Wenn der Speicherzugriff irgendwie optimal ist, kann AVX mit Trees größer als 200k Elementen helfen. Darunter fällt kaum eine Strafe (wenn Sie AVX nicht für etwas anderes nutzen). Es war die Nacht wert, dies zu benchmarken. Danke an alle Beteiligten :-)

0
hinzugefügt

Ich habe SSE2/AVX2 verwendet, um eine B + Baumsuche durchzuführen. Hier ist Code, um eine binäre Suche in einer vollständigen Cache-Zeile von 16 DWORDs in AVX2 durchzuführen:

// perf-critical: ensure this is 64-byte aligned. (a full cache line)
union bnode
{
    int32_t i32[16];
    __m256i m256[2];
};

// returns from 0 (if value < i32[0]) to 16 (if value >= i32[15]) 
unsigned bsearch_avx2(bnode const* const node, __m256i const value)
{
    __m256i const perm_mask = _mm256_set_epi32(7, 6, 3, 2, 5, 4, 1, 0);

   //compare the two halves of the cache line.

    __m256i cmp1 = _mm256_load_si256(&node->m256[0]);
    __m256i cmp2 = _mm256_load_si256(&node->m256[1]);

    cmp1 = _mm256_cmpgt_epi32(cmp1, value);//PCMPGTD
    cmp2 = _mm256_cmpgt_epi32(cmp2, value);//PCMPGTD

   //merge the comparisons back together.
    //
   //a permute is required to get the pack results back into order
   //because AVX-256 introduced that unfortunate two-lane interleave.
    //
   //alternately, you could pre-process your data to remove the need
   //for the permute.

    __m256i cmp = _mm256_packs_epi32(cmp1, cmp2);//PACKSSDW
    cmp = _mm256_permutevar8x32_epi32(cmp, perm_mask);//PERMD

   //finally create a move mask and count trailing
   //zeroes to get an index to the next node.

    unsigned mask = _mm256_movemask_epi8(cmp);//PMOVMSKB
    return _tzcnt_u32(mask)/2;//TZCNT
}

Am Ende erhalten Sie einen einzelnen hochgradig vorhersagbaren Zweig pro bnode , um zu testen, ob das Ende des Baums erreicht wurde.

Dies sollte für AVX-512 trivial skalierbar sein.

Um diese langsame PERMD Anweisung vorzuverarbeiten und loszuwerden, würde diese verwendet werden:

void preprocess_avx2(bnode* const node)
{
    __m256i const perm_mask = _mm256_set_epi32(3, 2, 1, 0, 7, 6, 5, 4);
    __m256i *const middle = (__m256i*)&node->i32[4];

    __m256i x = _mm256_loadu_si256(middle);
    x = _mm256_permutevar8x32_epi32(x, perm_mask);
    _mm256_storeu_si256(middle, x);
}
0
hinzugefügt
Ich freue mich darauf, dies zu testen, da wir auf AVX512-unterstützenden Geräten arbeiten werden. Ich dachte darüber nach, alle Daten in die letzte Ebene der Baumstruktur zu bringen und die ersten log2 (n) -1-Ebenen als schnellen Query-Accelerator zu verwenden; Anpassen von mehr Knoten in einer Cache-Zeile (keine Datenzeiger dort erforderlich, wenn der Baum statisch ist); Außerdem würde es die Anforderung zur Überprüfung auf Gleichheit bei jeder Überprüfung/Schleifen-Iteration von Knoten entfernen - nach Beendigung aller Iterationen wird nur eine == benötigt.
hinzugefügt der Autor user1610743, Quelle
Übrigens, gibt es einen besonderen Grund für die Speicherung von Verzweigungszeigern? Ich empfinde es als eine Verschwendung von Cache-Speicherplatz. Das Verschieben um 4 Byte anstelle von 1 für binäre Bäume sollte gut funktionieren.
hinzugefügt der Autor user1610743, Quelle
Ja, ich würde die Sachen nebeneinander stellen. Der Speicherplatz, der durch Nichtverwendung von Zeigern gespeichert wird, kann verwendet werden, um die Speicherzuweisung für dynamische Bäume zu überdimensionieren. Mit diesem Projekt sind wir mit statisch großen Bäumen in Ordnung. Das ist auch der Grund, warum ich darüber nachdenke, nur die letzte Ebene des Baumes zu verwenden, die nicht so gut funktionieren würde, wenn man sie einfügen müsste.
hinzugefügt der Autor user1610743, Quelle
Ich habe einen Benchmark dazu hinzugefügt, wie verschiedene Methoden beim Traversieren von Bäumen funktionieren! Danke, dass ihr mir mit dem AVX-Teil geholfen habt.
hinzugefügt der Autor user1610743, Quelle
Eine andere interessante Frage wäre das; wenn Sie die Strafe für falsch vorhergesagte Zweige löschen, indem Sie auf der Anweisungsseite effizienter sind, aber durch das Laden neuer Daten irgendwie gebunden sind; Man könnte zusätzliche Operationen an den Daten durchführen, während auf das Eintreffen der nächsten Daten gewartet wird. Ich könnte es mir vorstellen, um es in Game-Engines zu verwenden. Ich habe mich auch gefragt, wann der Prefetch-Befehl "Lade Cacheline +1" ausgegeben wird. Bisher bietet mein Baumspeicher-Layout keine Cacheline-Pfade wie DFS in cacheline chunks (vEB da). Mögliche Ve
hinzugefügt der Autor user1610743, Quelle
Ihre B-Tree-Knoten passen in eine einzige Cache-Zeile. Ich kann mir nicht vorstellen, dass die SSE (usw.) viel von einem Leistungsvorteil bieten würde, selbst wenn der B-Baum vollständig in den Cache passen würde (was wie ein ziemlich starker Fall zu sein scheint). Ich habe In-Memory-B-Bäume in Assembler erstellt, die dieselben Einschränkungen haben; ziemlich genau, man bekommt nur einen echten "einzelnen Zweig" pro Knoten, weil der Verzweigungs-Prädiktor es recht gut macht. Im schlimmsten Fall können Sie eine binäre Suche auf den Schlüsseln im Knoten durchführen; es gibt nur 6 Durchschnittswe
hinzugefügt der Autor Ira Baxter, Quelle
Wenn Ihre B-Tree-Knoten irgendwo sein können, wie können Sie die Zeiger vermeiden? Gehen Sie davon aus, dass der Baum zusammenhängend im Speicher ist?
hinzugefügt der Autor Ira Baxter, Quelle
Das ist keine binäre Suche innerhalb des B-Tree-Knotens. es ist eine O (N * log2 (N)) parallele Brute-Force-Suche, die wirklich gut für kleines N ist. (N = 2 ymm Vektoren in diesem Fall). (Der log2 (N) -Teil ist das Packen auf ein einzelnes skalares Bitmap. Obwohl für großes N, würden wir immer nur zu Byte-Elementen zusammenführen, dann einen abschließenden Verschmelzungsschritt nach vpmovmskb , und verwenden Sie mehrere _tzcnt_u64 . Also ich denke, es ist wirklich O (N) ). Wie auch immer, sieht für mich für diese Problemg
hinzugefügt der Autor Peter Cordes, Quelle
Ich bin gerade bei der Arbeit, also kann ich nicht nach dem Code suchen. Die SIMD ist im Grunde ein schneller Weg, um eine binäre Suche nach einer festen Anzahl von ganzen Zahlen durchzuführen, und reduziert diese Zweige. Mehr ist es nicht.
hinzugefügt der Autor Cory Nelson, Quelle
Verzweigungszeiger wurden in meinem Fall benötigt, aber es ist einfach, Fälle zu sehen, die sich darum herum optimieren lassen.
hinzugefügt der Autor Cory Nelson, Quelle
Und Ira, ein benutzerdefinierter Zuordner kann gemacht werden, um zusammenhängende Knoten und einen Basiszeiger bereitzustellen.
hinzugefügt der Autor Cory Nelson, Quelle
Habe meinen Code gefunden! Der Beitrag wurde aktualisiert.
hinzugefügt der Autor Cory Nelson, Quelle
Ich kann mir vorstellen, dass Sie Prefetching nutzen können, wenn Sie Ihren Algorithmus in Schritten erstellen und dazwischen etwas anderes machen. Ich bezweifle, dass es sonst nützlich wäre, aber ich wäre neugierig zu sehen, was Sie daraus machen.
hinzugefügt der Autor Cory Nelson, Quelle