Recientemente me encontré con la estructura de datos conocida como una lista de omisión . Parece tener un comportamiento muy similar al de un árbol de búsqueda binario.
¿Por qué querrías usar una lista de omisión sobre un árbol de búsqueda binario?
Recientemente me encontré con la estructura de datos conocida como una lista de omisión . Parece tener un comportamiento muy similar al de un árbol de búsqueda binario.
¿Por qué querrías usar una lista de omisión sobre un árbol de búsqueda binario?
Respuestas:
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.
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.
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.
"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.
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.
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.
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.
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.
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 :-)
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