Saltar lista vs. Árbol de búsqueda binaria

Respuestas:

257

Las listas de omisión son más susceptibles de acceso / modificación concurrente. Herb Sutter escribió un artículo sobre la estructura de datos en entornos concurrentes. Tiene más información en profundidad.

La implementación más utilizada de un árbol de búsqueda binaria es un árbol rojo-negro . Los problemas concurrentes aparecen cuando se modifica el árbol, a menudo necesita reequilibrarse. La operación de reequilibrio puede afectar grandes porciones del árbol, lo que requeriría un bloqueo de mutex en muchos de los nodos del árbol. Insertar un nodo en una lista de omisión está mucho más localizado, solo los nodos directamente vinculados al nodo afectado deben bloquearse.


Actualización de los comentarios de Jon Harrops

Leí la última programación simultánea en papel de Fraser y Harris sin bloqueos . Cosas realmente buenas si estás interesado en estructuras de datos sin bloqueo. El documento se centra en la memoria transaccional y una operación teórica de MCAS de comparación y intercambio de varias palabras. Ambos están simulados en software ya que ningún hardware los admite todavía. Estoy bastante impresionado de que hayan podido crear MCAS en software.

No encontré las cosas de la memoria transaccional particularmente convincentes, ya que requiere un recolector de basura. También la memoria transaccional de software está plagada de problemas de rendimiento. Sin embargo, estaría muy emocionado si la memoria transaccional de hardware alguna vez se vuelve común. Al final, sigue siendo investigación y no será de utilidad para el código de producción durante otra década más o menos.

En la sección 8.2, comparan el rendimiento de varias implementaciones de árbol concurrentes. Resumiré sus hallazgos. Vale la pena descargar el pdf ya que tiene algunos gráficos muy informativos en las páginas 50, 53 y 54.

  • Las listas de omisión de bloqueo son increíblemente rápidas. Se escalan increíblemente bien con la cantidad de accesos concurrentes. Esto es lo que hace que las listas de omisión sean especiales, otras estructuras de datos basadas en bloqueos tienden a croar bajo presión.
  • Las listas de omisión sin bloqueo son consistentemente más rápidas que las listas de omisión bloqueadas, pero solo apenas.
  • Las listas de saltos transaccionales son consistentemente 2-3 veces más lentas que las versiones con y sin bloqueo.
  • bloqueando árboles rojo-negro croar bajo acceso concurrente. Su rendimiento se degrada linealmente con cada nuevo usuario concurrente. De las dos implementaciones conocidas de bloqueo de árbol rojo-negro, una esencialmente tiene un bloqueo global durante el reequilibrio de árbol. El otro usa una escalada de bloqueo sofisticada (y complicada) pero aún no supera significativamente la versión de bloqueo global.
  • no existen árboles rojo-negros sin bloqueo (ya no es cierto, ver Actualización).
  • los árboles transaccionales rojo-negro son comparables con las listas de omisión transaccionales. Eso fue muy sorprendente y muy prometedor. Memoria transaccional, aunque más lenta si es mucho más fácil de escribir. Puede ser tan fácil como buscar y reemplazar rápidamente en la versión no concurrente.

Actualización
Aquí hay un documento sobre árboles sin bloqueo : Árboles rojo-negros sin bloqueo con CAS .
No lo he investigado profundamente, pero en la superficie parece sólido.

código_deft
fuente
3
Sin mencionar que en una lista de omisión no degenerada, aproximadamente el 50% de los nodos solo deberían tener un único enlace que haga que insertar y eliminar sea notablemente eficiente.
Adisak
2
El reequilibrio no requiere un bloqueo de mutex. Ver cl.cam.ac.uk/research/srg/netos/lock-free
Jon Harrop
3
@ Jon, sí y no. No se conocen implementaciones de árbol rojo-negro sin bloqueo. Fraser y Harris muestran cómo se implementa un árbol rojo-negro basado en memoria transaccional y su rendimiento. La memoria transaccional todavía está muy en el campo de la investigación, por lo que en el código de producción, un árbol rojo-negro aún necesitará bloquear grandes porciones del árbol.
deft_code
44
@deft_code: Intel anunció recientemente una implementación de memoria transaccional a través de TSX en Haswell. Esto puede resultar interesante con esas estructuras de datos sin bloqueo que mencionó.
Mike Bailey
2
Creo que la respuesta de Fizz está más actualizada (desde 2015) en lugar de esta respuesta (2012) y, por lo tanto, probablemente debería ser la respuesta preferida en este momento.
fnl
81

Primero, no puede comparar de manera justa una estructura de datos aleatoria con una que le brinde las garantías del peor de los casos.

Una lista de omisión es equivalente a un árbol de búsqueda binario balanceado al azar (RBST) en la forma en que se explica con más detalle en "Explorando la dualidad entre listas de omisión y árboles de búsqueda binaria" de Dean y Jones .

A la inversa, también puede tener listas de omisión deterministas que garanticen el peor rendimiento, cf. Munro y col.

Contrariamente a lo que algunos afirman anteriormente, puede tener implementaciones de árboles de búsqueda binarios (BST) que funcionen bien en la programación concurrente. Un problema potencial con los BST centrados en la concurrencia es que no puede obtener fácilmente lo mismo si tuviera garantías sobre el equilibrio como lo haría con un árbol rojo-negro (RB). (Pero las listas de omisión "estándar", es decir, aleatorizadas al azar, tampoco le brindan estas garantías.) Existe una compensación entre mantener el equilibrio en todo momento y un buen acceso concurrente (y fácil de programar), por lo que generalmente se usan árboles RB relajados cuando se desea buena concurrencia. La relajación consiste en no reequilibrar el árbol de inmediato. Para una encuesta un tanto anticuada (1998), vea Hanke's "The Performance of Concurrent Red-Black Tree Algorithms" [ps.gz] .

Una de las mejoras más recientes en estos es el llamado árbol cromático (básicamente tiene un peso tal que el negro sería 1 y el rojo sería cero, pero también permite valores intermedios). ¿Y cómo funciona un árbol cromático en comparación con la lista de omisión? Veamos lo que Brown et al. "Una técnica general para árboles sin bloqueo" (2014) tiene que decir:

Con 128 subprocesos, nuestro algoritmo supera la lista de omisión sin bloqueo de Java en un 13% a 156%, el árbol AVL basado en bloqueo de Bronson et al. en un 63% a 224%, y un RBT que usa memoria transaccional de software (STM) de 13 a 134 veces

EDITAR para agregar: la lista de saltos basada en bloqueo de Pugh, que fue comparada en "Programación concurrente sin bloqueo" de Fraser y Harris (2007) por acercarse a su propia versión sin bloqueo (un punto ampliamente insistido en la respuesta principal aquí), también está ajustado para una buena operación concurrente, cf. El "Mantenimiento concurrente de las listas de salto" de Pugh , aunque de una manera bastante moderada. Sin embargo, un artículo más reciente / 2009 "Un algoritmo simple de lista de salto optimista"por Herlihy et al., que propone una implementación de listas de omisión concurrente supuestamente más simple (que la de Pugh), criticó a Pugh por no proporcionar una prueba de corrección lo suficientemente convincente para ellos. Dejando a un lado este reparo (quizás demasiado pedante), Herlihy et al. muestran que su implementación más simple basada en el bloqueo de una lista de omisión en realidad no puede escalar, así como la implementación sin bloqueo del JDK de la misma, pero solo para una alta contención (50% de inserciones, 50% de eliminaciones y 0% de búsquedas) ... que Fraser y Harris no hizo ninguna prueba; Fraser y Harris solo probaron 75% de búsquedas, 12.5% ​​de inserciones y 12.5% ​​de eliminaciones (en la lista de omisión con ~ 500K elementos). La implementación más simple de Herlihy et al. también se acerca a la solución sin bloqueo del JDK en el caso de baja contención que probaron (70% de búsquedas, 20% de inserciones, 10% de eliminaciones); En realidad, superaron la solución sin bloqueo para este escenario cuando hicieron su lista de omisión lo suficientemente grande, es decir, pasaron de elementos de 200K a 2M, por lo que la probabilidad de contención en cualquier bloqueo se volvió insignificante. Hubiera sido bueno si Herlihy et al. había superado su problema con la prueba de Pugh y también había probado su implementación, pero lamentablemente no lo hicieron.

EDIT2: Encontré un motherlode (2015 publicado) de todos los puntos de referencia: Gramoli "Más de lo que siempre quisiste saber sobre la sincronización. Synchrobench, Midiendo el impacto de la sincronización en algoritmos concurrentes" : Aquí hay una imagen extraída relevante para esta pregunta.

ingrese la descripción de la imagen aquí

"Algo.4" es un precursor (versión anterior, 2011) de Brown et al. Mencionado anteriormente. (No sé cuánto mejor o peor es la versión 2014). "Algo.26" es el mencionado de Herlihy arriba; como puede ver, se destruye en las actualizaciones, y mucho peor en las CPU Intel utilizadas aquí que en las CPU Sun del documento original. "Algo.28" es ConcurrentSkipListMap del JDK; no funciona tan bien como cabría esperar en comparación con otras implementaciones de listas de omisión basadas en CAS. Los ganadores bajo alta contención son "Algo.2", un algoritmo basado en bloqueo (!!) descrito por Crain et al. en "Un árbol de búsqueda binaria amigable con la contención" y "Algo.30" es la "lista de salto giratoria" de "Estructuras de datos logarítmicas para multinúcleos" . ". Tenga en cuenta que Gramoli es coautor de estos tres documentos de algoritmos ganadores. "Algo.27" es la implementación en C ++ de la lista de omisión de Fraser.

La conclusión de Gramoli es que es mucho más fácil arruinar una implementación de árbol concurrente basada en CAS que arruinar una lista de omisión similar. Y según las cifras, es difícil no estar de acuerdo. Su explicación para este hecho es:

La dificultad en el diseño de un árbol sin bloqueo se deriva de la dificultad de modificar atómicamente múltiples referencias. Las listas de omisión consisten en torres vinculadas entre sí a través de punteros sucesores y en las que cada nodo apunta al nodo inmediatamente debajo de él. A menudo se consideran similares a los árboles porque cada nodo tiene un sucesor en la torre sucesora y debajo de ella, sin embargo, una distinción importante es que el puntero hacia abajo es generalmente inmutable, lo que simplifica la modificación atómica de un nodo. Esta distinción es probablemente la razón por la cual las listas de salto superan a los árboles bajo una fuerte contención como se observa en la Figura [arriba].

Superar esta dificultad fue una preocupación clave en el trabajo reciente de Brown et al. Tienen un documento completamente separado (2013) "Primitivas pragmáticas para estructuras de datos sin bloqueo" sobre la construcción de "primitivas" compuestas de múltiples registros LL / SC, que ellos llaman LLX / SCX, implementadas utilizando CAS (nivel de máquina). Brown y col. utilizó este bloque de creación LLX / SCX en su implementación de árbol concurrente 2014 (pero no en su 2011).

Creo que quizás también valga la pena resumir aquí las ideas fundamentales de la lista de omisión "sin puntos críticos" / amigables para la contención (CF). Adapta una idea esencial de los árboles RB relajados (y estructuras de datos similares con congruencia): las torres ya no se construyen inmediatamente después de la inserción, sino que se retrasan hasta que haya menos contención. Por el contrario, la eliminación de una torre alta puede crear muchas disputas; esto se observó ya en el documento concurrente de la lista de omisión de 1990 de Pugh, por lo que Pugh introdujo la inversión del puntero en la eliminación (un dato que la página de Wikipedia en las listas de omisión todavía no menciona hasta el día de hoy). La lista de omisión de CF lleva esto un paso más allá y retrasa la eliminación de los niveles superiores de una torre alta. Ambos tipos de operaciones retrasadas en las listas de omisión de CF se llevan a cabo mediante un subproceso similar al recolector de basura (basado en CAS), que sus autores llaman el "subproceso de adaptación".

El código Synchrobench (incluidos todos los algoritmos probados) está disponible en: https://github.com/gramoli/synchrobench . El último Brown et al. la implementación (no incluida en lo anterior) está disponible en http://www.cs.toronto.edu/~tabrown/chromatic/ConcurrentChromaticTreeMap.java ¿Alguien tiene una máquina de 32 núcleos disponible? J / K Mi punto es que pueden ejecutar estos ustedes mismos.

Efervescencia
fuente
12

Además, además de las respuestas dadas (facilidad de implementación combinada con rendimiento comparable a un árbol equilibrado). Me parece que la implementación transversal en orden (hacia adelante y hacia atrás) es mucho más simple porque una lista de omisión tiene efectivamente una lista vinculada dentro de su implementación.

Evan Teran
fuente
1
¿No es el recorrido en orden para un árbol bin tan simple como: "def func (node): func (left (node)); op (node); func (right (node))"?
Claudiu el
66
Claro, eso es cierto si desea recorrer todo en una llamada de función. pero se vuelve mucho más molesto si desea tener un recorrido de estilo iterador como en std :: map.
Evan Teran el
@Evan: No en un lenguaje funcional donde solo puedes escribir en 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;:? =). control no local iz awesom .. @ Jon: escribir en CPS es un dolor, pero ¿tal vez te refieres a las continuaciones? Los generadores son básicamente un caso especial de continuación para Python.
Claudiu
1
@Evan: sí, funciona siempre que el parámetro de nodo se corte del árbol durante una modificación. El recorrido en C ++ tiene la misma restricción.
deft_code
10

En la práctica, he descubierto que el rendimiento del árbol B en mis proyectos ha resultado ser mejor que las listas de omisión. Saltar listas parecen más fáciles de entender, pero la implementación de un árbol B no es que dura.

La única ventaja que conozco es que algunas personas inteligentes han descubierto cómo implementar una lista de omisión simultánea sin bloqueo que solo utiliza operaciones atómicas. Por ejemplo, Java 6 contiene la clase ConcurrentSkipListMap, y puede leer el código fuente si está loco.

Pero tampoco es demasiado difícil escribir una variante concurrente del árbol B, lo he visto hecho por otra persona, si se divide y fusiona los nodos de manera preventiva "por si acaso" mientras camina por el árbol, entonces no tendrá que hacerlo preocúpese por los puntos muertos y solo necesite mantener un bloqueo en dos niveles del árbol a la vez. La sobrecarga de sincronización será un poco más alta, pero el árbol B probablemente sea más rápido.

Jonathan
fuente
44
Creo que no deberías llamar a Binary Tree un B-Tree, hay un DS completamente diferente con ese nombre
Shihab Shahriar Khan el
8

Del artículo de Wikipedia que citó:

Las operaciones Θ (n), que nos obligan a visitar cada nodo en orden ascendente (como imprimir la lista completa) brindan la oportunidad de realizar una desleatorización detrás de escena de la estructura de niveles de la lista de omisión de manera óptima, llevando la lista de omisión al tiempo de búsqueda O (log n). [...] Una lista de omisión, sobre la cual no hemos realizado recientemente [cualquiera de estas] operaciones Θ (n), no proporciona las mismas garantías absolutas de rendimiento en el peor de los casos que las estructuras de datos de árbol equilibradas más tradicionales , porque siempre es posible (aunque con muy baja probabilidad) de que los lanzamientos de monedas utilizados para construir la lista de salto producirán una estructura mal equilibrada

EDITAR: por lo que es una compensación: las listas de salto usan menos memoria a riesgo de que puedan degenerar en un árbol desequilibrado.

Trigo Mitch
fuente
esta sería una razón contra el uso de la lista de omisión.
Claudiu
77
citando MSDN, "Las posibilidades [para 100 elementos de nivel 1] son ​​precisamente 1 en 1,267,650,600,228,229,401,496,703,205,376".
Peter
8
¿Por qué dirías que usan menos memoria?
Jonathan
1
@peterchen: Ya veo, gracias. Entonces, ¿esto no ocurre con las listas de salto deterministas? @Mitch: "Las listas de omisión usan menos memoria". ¿Cómo usan las listas de omisión menos memoria que los árboles binarios balanceados? Parece que tienen 4 punteros en cada nodo y nodos duplicados, mientras que los árboles tienen solo 2 punteros y no hay duplicados.
Jon Harrop
1
@ Jon Harrop: Los nodos en el nivel uno solo necesitan un puntero por nodo. Cualquier nodo en niveles superiores necesita solo dos punteros por nodo (uno para el siguiente nodo y otro para el nivel inferior), aunque, por supuesto, un nodo de nivel 3 significa que está utilizando 5 punteros en total para ese valor. Por supuesto, esto todavía absorberá mucha memoria (más que una búsqueda binaria si desea una lista de omisión inútil y tiene un gran conjunto de datos) ... pero creo que me falta algo ...
Brian
2

Las listas de salto se implementan usando listas.

Existen soluciones sin bloqueo para listas enlazadas individualmente y doblemente, pero no hay soluciones sin bloqueo que usen directamente solo CAS para cualquier estructura de datos O (logn).

Sin embargo, puede usar listas basadas en CAS para crear listas de omisión.

(Tenga en cuenta que MCAS, que se crea usando CAS, permite estructuras de datos arbitrarias y que se ha creado un árbol rojo-negro de prueba de concepto usando MCAS).

Entonces, por extraños que sean, resultan ser muy útiles :-)


fuente
55
"no hay soluciones sin bloqueo que utilizan directamente CAS solo para cualquier estructura de datos O (logn)". No es verdad. Para ver ejemplos de contadores, consulte cl.cam.ac.uk/research/srg/netos/lock-free
Jon Harrop
-1

Las listas de omisión tienen la ventaja de eliminar las cerraduras. Pero, el tiempo de ejecución depende de cómo se decida el nivel de un nuevo nodo. Por lo general, esto se hace usando Random (). En un diccionario de 56000 palabras, la lista de omisión tomó más tiempo que un árbol desplegado y el árbol tomó más tiempo que una tabla hash. Los dos primeros no pudieron coincidir con el tiempo de ejecución de la tabla hash. Además, la matriz de la tabla hash también se puede eliminar de forma simultánea.

La Lista de saltos y listas ordenadas similares se utilizan cuando se necesita la localidad de referencia. Por ejemplo: encontrar vuelos al lado y antes de una fecha en una aplicación.

Un árbol de búsqueda binario inmemory es genial y se usa con más frecuencia.

Saltar lista Vs Splay Tree Vs Hash Table Tiempo de ejecución en el diccionario find op

Harisankar Krishna Swamy
fuente
Eché un vistazo rápido y sus resultados parecen mostrar SkipList como más rápido que SplayTree.
Chinasaur
Es engañoso asumir la aleatorización como parte de la lista de omisión. Cómo se omiten los elementos es crucial. La aleatorización se agrega para las estructuras probabilísticas.
user568109