Salta elenco vs. Albero di ricerca binario


Risposte:


257

Le liste di salto sono più suscettibili di accesso / modifica simultanei. Herb Sutter ha scritto un articolo sulla struttura dei dati in ambienti concorrenti. Ha più informazioni approfondite.

L'implementazione più frequentemente utilizzata di un albero di ricerca binario è un albero rosso-nero . I problemi concomitanti si presentano quando l'albero viene modificato spesso deve essere riequilibrato. L'operazione di ribilanciamento può influire su ampie porzioni dell'albero, il che richiederebbe un blocco mutex su molti nodi dell'albero. L'inserimento di un nodo in un elenco di salto è molto più localizzato, solo i nodi collegati direttamente al nodo interessato devono essere bloccati.


Aggiornamento dai commenti di Jon Harrops

Ho letto l'ultimo articolo di Fraser e Harris Programmazione concorrente senza blocchi . Roba davvero buona se sei interessato a strutture dati senza lock. Il documento si concentra sulla memoria transazionale e su un'operazione teorica MCAS multi-confronto-e-scambio di parole. Entrambi sono simulati nel software in quanto nessun hardware li supporta ancora. Sono abbastanza impressionato dal fatto che siano stati in grado di creare MCAS nel software.

Non ho trovato particolarmente interessante la memoria transazionale in quanto richiede un garbage collector. Anche la memoria transazionale del software è afflitta da problemi di prestazioni. Tuttavia, sarei molto entusiasta se la memoria transazionale dell'hardware diventasse mai comune. Alla fine è ancora ricerca e non sarà utile per il codice di produzione per un altro decennio o giù di lì.

Nella sezione 8.2 confrontano le prestazioni di diverse implementazioni simultanee dell'albero. Riassumo i loro risultati. Vale la pena scaricare il pdf in quanto presenta alcuni grafici molto istruttivi alle pagine 50, 53 e 54.

  • Il blocco degli elenchi di salto è follemente veloce. Scalano incredibilmente bene con il numero di accessi simultanei. Questo è ciò che rende speciali gli skip list, altre strutture di dati basate su blocchi tendono a gracchiare sotto pressione.
  • Le liste di salto senza blocco sono sempre più veloci rispetto al blocco delle liste di salto, ma solo a malapena.
  • gli elenchi di salti transazionali sono costantemente 2-3 volte più lenti delle versioni bloccanti e non bloccanti.
  • bloccando gli alberi rosso-neri gracchiano sotto accesso simultaneo. Le loro prestazioni diminuiscono in modo lineare con ogni nuovo utente simultaneo. Delle due implementazioni di alberi rosso-nero di blocco note, una ha essenzialmente un blocco globale durante il riequilibrio degli alberi. L'altro utilizza escalation di blocchi sofisticati (e complicati) ma non esegue ancora in modo significativo la versione di blocco globale.
  • non esistono alberi rosso-neri senza blocco (non più vero, vedi Aggiornamento).
  • gli alberi rosso-neri transazionali sono comparabili con i salti-liste transazionali. È stato molto sorprendente e molto promettente. Memoria transazionale, sebbene più lenta se molto più facile da scrivere. Può essere facile come la ricerca rapida e la sostituzione sulla versione non concorrente.

Aggiornamento
Ecco un documento sugli alberi senza lock : Alberi rosso-neri senza lock usando CAS .
Non ci ho studiato a fondo, ma in superficie sembra solido.


3
Per non parlare del fatto che in uno skiplist non degenerato, circa il 50% dei nodi dovrebbe avere un solo collegamento che rende l'inserimento e l'eliminazione notevolmente efficienti.
Adisak,

2
Il riequilibrio non richiede un blocco mutex. Vedi cl.cam.ac.uk/research/srg/netos/lock-free
Jon Harrop

3
@Jon, sì e no. Non ci sono implementazioni albero rosso-nero senza lock conosciute. Fraser e Harris mostrano come viene implementato un albero rosso-nero basato sulla memoria transazionale e le sue prestazioni. La memoria transazionale è ancora molto nell'arena della ricerca, quindi nel codice di produzione, un albero rosso-nero dovrà ancora bloccare grandi porzioni dell'albero.
deft_code

4
@deft_code: Intel ha recentemente annunciato un'implementazione della memoria transazionale tramite TSX su Haswell. Ciò può rivelarsi interessante rispetto alle strutture di dati senza blocco menzionate.
Mike Bailey,

2
Penso che la risposta di Fizz sia più aggiornata (dal 2015) piuttosto che questa risposta (2012) e quindi probabilmente dovrebbe essere la risposta preferita ormai.
fnl,

81

Innanzitutto, non è possibile confrontare in modo equo una struttura di dati randomizzata con una che offre garanzie nel caso peggiore.

Una skip list equivale a un albero di ricerca binaria bilanciato in modo casuale (RBST) nel modo che è spiegato in modo più dettagliato in "Exploring the Duality Between Skip Lists and Binary Search Trees" di Dean e Jones .

Al contrario, puoi anche avere skip list deterministici che garantiscono prestazioni nel caso peggiore, cfr. Munro et al.

Contrariamente a quanto sostengono alcuni sopra, è possibile avere implementazioni di alberi di ricerca binari (BST) che funzionano bene nella programmazione concorrente. Un potenziale problema con i BST focalizzati sulla concorrenza è che non si possono facilmente ottenere le stesse garanzie sul bilanciamento come si farebbe da un albero rosso-nero (RB). (Ma gli elenchi skip "standard", ovvero randomizzati, non offrono nemmeno queste garanzie.) C'è un compromesso tra il mantenimento del bilanciamento in ogni momento e un buon accesso simultaneo (e facile da programmare), quindi di solito vengono utilizzati alberi RB rilassati quando si desidera una buona concorrenza. Il rilassamento consiste nel non riequilibrare subito l'albero. Per un sondaggio un po 'datato (1998) vedi "The Performance of Concurrent Red-Black Tree Algorithms" di Hanke [ps.gz] .

Uno dei miglioramenti più recenti su questi è il cosiddetto albero cromatico (in pratica hai un peso tale che il nero sarebbe 1 e il rosso sarebbe zero, ma permetti anche valori in mezzo). E come va un albero cromatico contro la lista salta? Vediamo cosa Brown et al. "Una tecnica generale per alberi non bloccanti" (2014) deve dire:

con 128 thread, il nostro algoritmo supera la skiplist non bloccante di Java dal 13% al 156%, l'albero AVL basato su blocco di Bronson et al. dal 63% al 224% e un RBT che utilizza la memoria transazionale del software (STM) da 13 a 134 volte

EDIT da aggiungere: la lista skip basata su lock di Pugh, che è stata comparata in Fraser and Harris (2007) "Concurrent Programming Without Lock" come prossima alla loro versione senza lock (un punto ampiamente insistito nella risposta in alto qui), è anche ottimizzato per una buona operazione simultanea, cfr. "Concurrent Maintenance of Skip Lists" di Pugh , anche se in modo piuttosto delicato. Tuttavia, un nuovo documento / 2009 "Un semplice algoritmo ottimistico salta l'elenco"di Herlihy et al., che propone un'implementazione presumibilmente più semplice (rispetto a quella di Pugh) di skip list simultanee, ha criticato Pugh per non aver fornito una prova di correttezza abbastanza convincente per loro. Lasciando da parte questo scrupolo (forse troppo pedante), Herlihy et al. mostra che la loro semplice implementazione basata su lock di un skip list in realtà non riesce a ridimensionarsi così come la sua implementazione senza lock di JDK, ma solo per contese elevate (50% di inserti, 50% di eliminazioni e 0% di ricerche) ... quale Fraser e Harris non ha provato affatto; Fraser e Harris hanno testato solo il 75% di ricerche, il 12,5% di inserti e il 12,5% di eliminazioni (nell'elenco dei salti con ~ 500K elementi). L'implementazione più semplice di Herlihy et al. si avvicina anche alla soluzione senza blocco del JDK in caso di contesa bassa che hanno testato (70% di ricerche, 20% di inserti, 10% di eliminazioni); hanno effettivamente battuto la soluzione senza blocco per questo scenario quando hanno reso la loro lista dei salti abbastanza grande, cioè passando da 200K a 2M elementi, in modo che la probabilità di contesa su qualsiasi blocco diventasse trascurabile. Sarebbe stato bello se Herlihy et al. avevano superato i loro problemi con le prove di Pugh e avevano testato anche la sua implementazione, ma purtroppo non l'hanno fatto.

EDIT2: Ho trovato un motherlode (pubblicato nel 2015) di tutti i benchmark: Gramoli "Più di quanto tu abbia mai voluto sapere sulla sincronizzazione. Synchrobench, Misurare l'impatto della sincronizzazione sugli algoritmi concorrenti" : Ecco un'immagine estratta pertinente a questa domanda.

inserisci qui la descrizione dell'immagine

"Algo.4" è un precursore (versione precedente, 2011) di Brown et al. Di cui sopra. (Non so quanto sia migliore o peggiore la versione 2014). "Algo.26" è quello di cui parla Herlihy sopra; come puoi vedere viene cestinato sugli aggiornamenti, e molto peggio sulle CPU Intel utilizzate qui che sulle CPU Sun dal documento originale. "Algo.28" è ConcurrentSkipListMap dal JDK; non funziona come si potrebbe sperare rispetto ad altre implementazioni di skip list basate su CAS. I vincitori in contesa alta sono "Algo.2", un algoritmo basato su lock (!!) descritto da Crain et al. in "Un albero di ricerca binario a prova di contesa" e "Algo.30" è la "lista di scorrimento rotante" da "Strutture di dati logaritmici per multicore" . ". Si informa che Gramoli è coautore di tutti e tre questi articoli dell'algoritmo vincitore. "Algo.27" è l'implementazione C ++ dell'elenco skip di Fraser.

La conclusione di Gramoli è che è molto più facile rovinare un'implementazione ad albero simultanea basata su CAS piuttosto che rovinare un elenco di salti simile. E sulla base delle cifre, è difficile non essere d'accordo. La sua spiegazione per questo fatto è:

La difficoltà di progettare un albero privo di blocchi deriva dalla difficoltà di modificare atomicamente più riferimenti. Le liste di salto sono costituite da torri collegate tra loro tramite puntatori successivi e in cui ciascun nodo punta al nodo immediatamente sotto di esso. Sono spesso considerati simili agli alberi perché ogni nodo ha un successore nella torre del successore e sotto di esso, tuttavia, una distinzione principale è che il puntatore verso il basso è generalmente immutabile, semplificando quindi la modifica atomica di un nodo. Questa distinzione è probabilmente il motivo per cui gli elenchi saltati superano gli alberi in contese pesanti, come osservato nella Figura [sopra].

Superare questa difficoltà è stata una preoccupazione fondamentale nel recente lavoro di Brown et al. Hanno un documento completamente separato (2013) "Primitive pragmatiche per strutture di dati non bloccanti" sulla costruzione di "primitive" composte multi-record LL / SC, che chiamano LLX / SCX, implementate a loro volta utilizzando (a livello di macchina) CAS. Brown et al. hanno utilizzato questo building block LLX / SCX nella loro implementazione simultanea dell'albero 2014 (ma non nel 2011).

Penso che valga la pena di riassumere qui anche le idee fondamentali dell'elenco di salto "no hot spot" / contention-friendly (CF). Aggiunge un'idea essenziale dagli alberi RB rilassati (e simili strutture di dati freddi di concrrenza): le torri non sono più costruite immediatamente dopo l'inserimento, ma ritardate fino a quando non c'è meno contesa. Al contrario, l'eliminazione di un'alta torre può creare molte contese; questo è stato osservato fin dal documento simultaneo di skip-list del 1990 di Pugh, motivo per cui Pugh ha introdotto l'inversione del puntatore sulla cancellazione (un bocconcino che la pagina di Wikipedia sugli elenchi di salti non menziona ancora oggi, purtroppo). L'elenco skip CF fa un ulteriore passo avanti e ritarda l'eliminazione dei livelli superiori di un'alta torre. Entrambi i tipi di operazioni ritardate negli elenchi skip CF vengono eseguiti da un thread separato simile a garbage collector, basato su CAS, che i suoi autori chiamano "thread di adattamento".

Il codice Synchrobench (inclusi tutti gli algoritmi testati) è disponibile su: https://github.com/gramoli/synchrobench . L'ultima Brown et al. l'implementazione (non inclusa in precedenza) è disponibile all'indirizzo http://www.cs.toronto.edu/~tabrown/chromatic/ConcurrentChromaticTreeMap.java Qualcuno ha a disposizione una macchina di 32+ core? J / K Il mio punto è che puoi gestirli da soli.


12

Inoltre, oltre alle risposte fornite (facilità di implementazione combinata con prestazioni comparabili a un albero bilanciato). Trovo che implementare l'attraversamento in ordine (avanti e indietro) sia molto più semplice perché un elenco di salto ha effettivamente un elenco collegato all'interno della sua implementazione.


1
l'attraversamento in ordine di un albero bin non è semplice come: "def func (nodo): func (sinistra (nodo)); op (nodo); func (destra (nodo))"?
Claudiu,

6
Certo, è vero se vuoi attraversare tutto in una chiamata di funzione. ma diventa molto più fastidioso se vuoi avere un attraversamento in stile iteratore come in std :: map.
Evan Teran,

@Evan: non in un linguaggio funzionale in cui puoi semplicemente scrivere in CPS.
Jon Harrop,

@Evan: def iterate(node): for child in iterate(left(node)): yield child; yield node; for child in iterate(right(node)): yield child;? =). controllo non locale aw stupendo .. @Jon: scrivere in CPS è un dolore, ma forse vuoi dire con continuazioni? i generatori sono fondamentalmente un caso speciale di continuazioni per Python.
Claudiu,

1
@Evan: sì, funziona fintanto che il parametro del nodo viene tagliato dall'albero durante una modifica. Il traversal in C ++ ha lo stesso vincolo.
deft_code

10

In pratica, ho scoperto che le prestazioni di B-tree nei miei progetti sono andate meglio delle skip list. Le liste di salti sembrano più facili da capire ma l'implementazione di un albero a B non è così difficile.

L'unico vantaggio che conosco è che alcune persone intelligenti hanno capito come implementare un elenco di salto simultaneo senza blocco che utilizza solo operazioni atomiche. Ad esempio, Java 6 contiene la classe ConcurrentSkipListMap e puoi leggere il codice sorgente se sei pazzo.

Ma non è neanche troppo difficile scrivere una variante B-tree simultanea - l'ho visto fare da qualcun altro - se dividi e unisci preventivamente nodi "per ogni evenienza" mentre cammini lungo l'albero, non dovrai preoccuparsi di deadlock e sempre e solo bisogno di tenere un lucchetto su due livelli dell'albero alla volta. L'overhead di sincronizzazione sarà leggermente più alto ma l'albero B è probabilmente più veloce.


4
Penso che non dovresti chiamare Binary Tree un B-Tree, c'è un DS completamente diverso con quel nome
Shihab Shahriar Khan,

8

Dalla Wikipedia articolo lei ha citato:

Θ (n) operazioni, che ci costringono a visitare tutti i nodi in ordine crescente (come stampare l'intero elenco) offrono l'opportunità di eseguire una derandomizzazione dietro le quinte della struttura di livello dell'elenco skip in modo ottimale, portando la lista salta al tempo di ricerca O (log n). [...] Un skip list, sul quale non abbiamo eseguito di recente [nessuna di queste] Θ (n) operazioni, non fornisce le stesse garanzie assolute di prestazioni nel caso peggiore delle strutture di dati ad albero bilanciato più tradizionali , perché è sempre possibile (sebbene con probabilità molto bassa) che i lanci di monete usati per costruire la lista dei salti producano una struttura mal bilanciata

EDIT: quindi è un compromesso: gli Skip List usano meno memoria a rischio di degenerare in un albero sbilanciato.


questo sarebbe un motivo per non usare la lista salta.
Claudiu,

7
citando MSDN, "Le probabilità [per 100 elementi di livello 1] sono precisamente 1 su 1.267.650.600.228.229.401.496.703.205.376".
Peter

8
Perché diresti che usano meno memoria?
Jonathan,

1
@peterchen: vedo, grazie. Quindi questo non si verifica con gli skip list deterministici? @Mitch: "Gli elenchi di salto utilizzano meno memoria". In che modo gli elenchi di salto utilizzano meno memoria rispetto agli alberi binari bilanciati? Sembra che abbiano 4 puntatori in ogni nodo e nodi duplicati mentre gli alberi hanno solo 2 puntatori e nessun duplicato.
Jon Harrop,

1
@Jon Harrop: i nodi al primo livello richiedono solo un puntatore per nodo. Tutti i nodi di livello superiore richiedono solo due puntatori per nodo (uno al nodo successivo e uno al livello sottostante), sebbene ovviamente un nodo di livello 3 significhi che stai usando 5 puntatori totali per quell'unico valore. Ovviamente, questo risucchia ancora molta memoria (più di una ricerca binaria se si desidera un elenco di salto non inutile e si ha un set di dati di grandi dimensioni) ... ma penso che mi manchi qualcosa ...
Brian

2

Le liste di salto sono implementate usando le liste.

Esistono soluzioni senza blocco per elenchi collegati singolarmente e doppiamente, ma non esistono soluzioni senza blocco che utilizzano direttamente solo CAS per qualsiasi struttura di dati O (logn).

È tuttavia possibile utilizzare elenchi basati su CAS per creare elenchi di salto.

(Si noti che MCAS, che è stato creato utilizzando CAS, consente strutture di dati arbitrarie e un albero rosso-nero di prova del concetto era stato creato utilizzando MCAS).

Quindi, per quanto siano strani, risultano molto utili :-)


5
"non esistono soluzioni senza blocco che utilizzano direttamente solo CAS per qualsiasi struttura di dati O (logn)". Non vero. Per esempi contrari vedere cl.cam.ac.uk/research/srg/netos/lock-free
Jon Harrop

-1

Gli Skip List hanno il vantaggio di rimuovere i blocchi. Tuttavia, il tempo di autonomia dipende da come viene deciso il livello di un nuovo nodo. Di solito questo viene fatto usando Random (). Su un dizionario di 56000 parole, saltare la lista ha impiegato più tempo di un albero di visualizzazione e l'albero ha impiegato più tempo di una tabella di hash. I primi due non potevano corrispondere al runtime della tabella hash. Inoltre, l'array della tabella hash può anche essere privato del blocco.

Gli Skip List e gli elenchi ordinati simili vengono utilizzati quando è necessaria la località di riferimento. Ad esempio: ricerca di voli prima e prima di una data in un'applicazione.

Un albero di splay di ricerca binaria immemorabile è fantastico e più frequentemente utilizzato.

Skip List Vs Splay Tree Vs Hash Table Runtime sul dizionario find op


Ho dato una rapida occhiata e i tuoi risultati sembrano mostrare SkipList più velocemente di SplayTree.
Chinasaur,

È fuorviante assumere la randomizzazione come parte dell'elenco di salto. Il modo in cui gli elementi vengono ignorati è fondamentale. La randomizzazione viene aggiunta per le strutture probabilistiche.
user568109
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.