Recentemente, deparei com a estrutura de dados conhecida como lista de pulos . Parece ter um comportamento muito semelhante a uma árvore de pesquisa binária.
Por que você gostaria de usar uma lista de pulos em uma árvore de pesquisa binária?
Recentemente, deparei com a estrutura de dados conhecida como lista de pulos . Parece ter um comportamento muito semelhante a uma árvore de pesquisa binária.
Por que você gostaria de usar uma lista de pulos em uma árvore de pesquisa binária?
Respostas:
As listas de ignorados são mais passíveis de acesso / modificação simultânea. Herb Sutter escreveu um artigo sobre estrutura de dados em ambientes concorrentes. Tem mais informações detalhadas.
A implementação mais frequentemente usada de uma árvore de pesquisa binária é uma árvore vermelha e preta . Os problemas simultâneos surgem quando a árvore é modificada e geralmente precisa se reequilibrar. A operação de reequilíbrio pode afetar grandes partes da árvore, o que exigiria um bloqueio mutex em muitos dos nós da árvore. A inserção de um nó em uma lista de ignorados é muito mais localizada, apenas os nós diretamente vinculados ao nó afetado precisam ser bloqueados.
Atualização dos comentários de Jon Harrops
Li o último artigo sobre Fraser e Harris, Programação simultânea sem bloqueios . Coisas realmente boas se você estiver interessado em estruturas de dados sem bloqueio. O artigo enfoca a memória transacional e um MCAS de operação múltipla, comparação e troca de palavras teóricas. Ambos são simulados no software, pois nenhum hardware os suporta ainda. Estou bastante impressionado que eles conseguiram criar o MCAS em software.
Não achei o material da memória transacional particularmente atraente, pois requer um coletor de lixo. Além disso , a memória transacional do software é afetada por problemas de desempenho. No entanto, eu ficaria muito animado se a memória transacional de hardware se tornar comum. No final, ainda é pesquisa e não será útil para o código de produção por mais uma década.
Na seção 8.2, eles comparam o desempenho de várias implementações simultâneas em árvore. Vou resumir suas descobertas. Vale a pena fazer o download do pdf, pois possui alguns gráficos muito informativos nas páginas 50, 53 e 54.
Atualizar
Aqui está um artigo sobre árvores sem bloqueio : Árvores vermelho-preto sem bloqueio usando o CAS .
Eu não olhei profundamente, mas na superfície parece sólido.
Primeiro, você não pode comparar uma estrutura de dados aleatória com uma que ofereça garantias de pior caso.
Uma lista de pulos é equivalente a uma árvore de pesquisa binária aleatoriamente balanceada (RBST) da maneira que é explicada em mais detalhes em Dean and Jones "Explorando a dualidade entre listas de pulos e árvores de pesquisa binária" .
Por outro lado, você também pode ter listas de pulos deterministas que garantem o pior desempenho possível, cf. Munro et al.
Ao contrário do que algumas afirmações acima, você pode ter implementações de árvores de pesquisa binária (BST) que funcionam bem em programação simultânea. Um problema potencial com as BSTs focadas na simultaneidade é que você não pode obter facilmente as mesmas garantias de balanceamento, como faria em uma árvore RB (vermelho-preto). (Mas as listas de pular "padrão", ou seja, com divisão aleatória, também não oferecem essas garantias.) Há uma troca entre manter o equilíbrio em todos os momentos e um bom acesso simultâneo (e fácil de programar); portanto, geralmente são usadas árvores de RB relaxadas quando boa concorrência é desejada. O relaxamento consiste em não reequilibrar a árvore imediatamente. Para uma pesquisa um pouco datada (1998), consulte "O desempenho de algoritmos simultâneos de árvores vermelhas e negras" de Hanke [ps.gz] .
Uma das melhorias mais recentes sobre elas é a chamada árvore cromática (basicamente você tem um peso tal que o preto seria 1 e o vermelho seria zero, mas você também permite valores intermediários). E como uma árvore cromática se sai contra a lista de pulos? Vamos ver o que Brown et al. "Uma técnica geral para árvores sem bloqueio" (2014) deve dizer:
com 128 threads, nosso algoritmo supera o skiplist sem bloqueio de Java em 13% a 156%, a árvore AVL baseada em bloqueio de Bronson et al. 63% a 224% e um RBT que usa a memória transacional de software (STM) por 13 a 134 vezes
EDIT para adicionar: a lista de pulos baseada em bloqueio de Pugh, que foi comparada em "Programação simultânea sem bloqueio" de Fraser e Harris (2007) como se aproximando de sua própria versão sem bloqueio (um ponto amplamente insistido na resposta principal aqui), também é ajustado para uma boa operação simultânea, cf. "Manutenção simultânea de Skip Lists" de Pugh , embora de uma maneira bastante moderada. No entanto, um artigo mais recente de 2009 "Um algoritmo de lista simplificada otimista simples"por Herlihy et al., que propõe uma implementação supostamente mais simples (do que a de Pugh) de listas de pulos simultâneas, criticou Pugh por não fornecer uma prova de correção convincente o suficiente para elas. Deixando de lado esse problema (talvez pedante demais), Herlihy et al. mostram que a implementação mais simples de uma lista de ignorados baseada em bloqueios falha na escala, bem como a implementação sem bloqueios do JDK, mas apenas para alta contenção (inserções de 50%, exclusões de 50% e pesquisas de 0%) ... que Fraser e Harris não testou nada; Fraser e Harris testaram apenas 75% de pesquisas, 12,5% de inserções e 12,5% de exclusões (na lista de faixas com ~ 500 mil elementos). A implementação mais simples de Herlihy et al. também se aproxima da solução sem bloqueios do JDK no caso de baixa contenção testada (70% pesquisas, 20% inserções, 10% exclusões); eles realmente venceram a solução sem bloqueios para esse cenário quando aumentaram sua lista de pulos, ou seja, passando de 200 mil a 2 milhões de elementos, para que a probabilidade de contenção em qualquer bloqueio se tornasse insignificante. Teria sido bom se Herlihy et al. haviam superado sua dificuldade com a prova de Pugh e testado sua implementação também, mas, infelizmente, eles não fizeram isso.
EDIT2: Encontrei um código-mãe (publicado em 2015) de todos os parâmetros de referência: "Mais do que você jamais quis saber sobre sincronização .
"Algo.4" é um precursor (versão mais antiga de 2011) de Brown et al. Mencionados acima. (Eu não sei o quão melhor ou pior a versão de 2014 é). "Algo.26" é o Herlihy mencionado acima; como você pode ver, isso é prejudicado pelas atualizações e muito pior nas CPUs Intel usadas aqui do que nas CPUs Sun do documento original. "Algo.28" é ConcurrentSkipListMap do JDK; não funciona tão bem quanto se poderia esperar em comparação com outras implementações de lista de ignorados baseadas em CAS. Os vencedores sob alta contenção são "Algo.2", um algoritmo baseado em bloqueio (!!) descrito por Crain et al. em "Uma árvore de pesquisa binária de contenção" e "Algo.30" é o "skiplist rotativo" de "Estruturas de dados logarítmicas para multicores" . ". Esteja ciente de que Gramoli é co-autor de todos esses três trabalhos sobre o algoritmo vencedor. "Algo.27" é a implementação em C ++ da lista de ignorados de Fraser.
A conclusão de Gramoli é que é muito mais fácil estragar uma implementação de árvore simultânea baseada em CAS do que estragar uma lista de pulos semelhante. E com base nos números, é difícil discordar. Sua explicação para esse fato é:
A dificuldade em projetar uma árvore livre de bloqueios decorre da dificuldade de modificar várias referências atomicamente. As listas de ignorados consistem em torres ligadas entre si por meio de ponteiros sucessores e nas quais cada nó aponta para o nó imediatamente abaixo dele. Eles são freqüentemente considerados semelhantes às árvores, porque cada nó tem um sucessor na torre sucessora e abaixo dela, no entanto, uma grande distinção é que o ponteiro descendente é geralmente imutável, simplificando a modificação atômica de um nó. Essa distinção é provavelmente a razão pela qual as listas de pulos superam as árvores sob forte contenção, conforme observado na Figura [acima].
A superação dessa dificuldade foi uma preocupação importante no trabalho recente de Brown et al. Eles têm um artigo inteiro (2013) "Primitivas Pragmáticas para Estruturas de Dados Não Bloqueadas" sobre a construção de "primitivas" compostas LL / SC com vários registros, que eles chamam de LLX / SCX, implementadas por meio do CAS (no nível da máquina). Brown et al. usaram esse componente básico do LLX / SCX em sua implementação em árvore simultânea de 2014 (mas não em 2011).
Eu acho que talvez também valha a pena resumir aqui as idéias fundamentais da lista de pulos "no hot spot" / convict-friendly (CF). Ele adiciona uma idéia essencial das árvores relaxadas da RB (e estruturas semelhantes de dados de concreto): as torres não são mais construídas imediatamente após a inserção, mas são adiadas até que haja menos contenção. Por outro lado, a exclusão de uma torre alta pode criar muitas contenções; isso foi observado desde o artigo simultâneo de lista de ignorados de Pugh, de 1990, e é por isso que Pugh introduziu a reversão de ponteiro na exclusão (um petisco que a página da Wikipedia nas listas de ignorados ainda não menciona até hoje). A lista de pulos de CF dá um passo adiante e atrasa a exclusão dos níveis superiores de uma torre alta. Os dois tipos de operações atrasadas nas listas de ignorados do CF são executados por um thread do tipo coletor de lixo separado (baseado em CAS), que seus autores chamam de "thread de adaptação".
O código Synchrobench (incluindo todos os algoritmos testados) está disponível em: https://github.com/gramoli/synchrobench . O mais recente Brown et al. a implementação (não incluída acima) está disponível em http://www.cs.toronto.edu/~tabrown/chromatic/ConcurrentChromaticTreeMap.java Alguém tem uma máquina com mais de 32 núcleos disponível? J / K O que quero dizer é que você pode executar isso sozinho.
Além disso, além das respostas dadas (facilidade de implementação combinada com desempenho comparável a uma árvore equilibrada). Acho que implementar a travessia em ordem (para frente e para trás) é muito mais simples, porque uma lista de pulos efetivamente tem uma lista vinculada dentro de sua implementação.
def iterate(node): for child in iterate(left(node)): yield child; yield node; for child in iterate(right(node)): yield child;
:? =). controle não local iz awesom .. @ Jon: escrever em CPS é uma dor, mas talvez você queira dizer com continuações? geradores são basicamente um caso especial de continuação para python.
Na prática, descobri que o desempenho da árvore B em meus projetos funcionou melhor do que as listas de ignorados. Ir listas que parecem mais fáceis de entender, mas a implementação de um B-árvore não é que difícil.
A única vantagem que eu conheço é que algumas pessoas inteligentes descobriram como implementar uma lista de pulos simultâneos sem bloqueios que usa apenas operações atômicas. Por exemplo, o Java 6 contém a classe ConcurrentSkipListMap e você pode ler o código-fonte se estiver louco.
Mas também não é muito difícil escrever uma variante simultânea da árvore B - eu já vi isso por outra pessoa - se você separar e mesclar preventivamente os nós "apenas por precaução" enquanto você caminha pela árvore, não precisará se preocupe com os impasses e só precisará segurar uma trava em dois níveis da árvore por vez. A sobrecarga de sincronização será um pouco maior, mas a árvore B provavelmente é mais rápida.
No artigo da Wikipedia que você citou:
Operations (n) operações, que nos forçam a visitar todos os nós em ordem crescente (como imprimir a lista inteira), oferecem a oportunidade de realizar uma des aleatorização dos bastidores da estrutura de níveis da lista de ignorados da melhor maneira possível, trazendo a lista de pulos para o tempo de pesquisa O (log n). [...] Uma lista de pulos, na qual recentemente não realizamos [nenhuma dessas operações] Θ (n), não fornece as mesmas garantias absolutas de desempenho de pior caso que as estruturas de dados em árvore balanceadas mais tradicionais , porque sempre é possível (embora com probabilidade muito baixa) de que os lançamentos de moedas usados para criar a lista de pulos produzam uma estrutura mal equilibrada
EDIT: portanto, é um trade-off: Skip Lists usam menos memória com o risco de que possam degenerar em uma árvore desequilibrada.
As listas de ignorados são implementadas usando listas.
Existem soluções sem bloqueio para listas individuais e duplamente vinculadas - mas não há soluções sem bloqueio que usam diretamente apenas o CAS para qualquer estrutura de dados O (logn).
No entanto, você pode usar listas baseadas em CAS para criar listas de ignorados.
(Observe que o MCAS, criado usando o CAS, permite estruturas de dados arbitrárias e uma prova de conceito de árvore vermelho-preta foi criada usando o MCAS).
Por mais estranhos que sejam, eles se tornam muito úteis :-)
As Listas de saltos têm a vantagem de remover trava. Mas, o tempo de execução depende de como o nível de um novo nó é decidido. Geralmente isso é feito usando Random (). Em um dicionário de 56.000 palavras, a lista de pulos levou mais tempo que uma árvore de espalhamento e a árvore levou mais tempo que uma tabela de hash. Os dois primeiros não puderam corresponder ao tempo de execução da tabela de hash. Além disso, a matriz da tabela de hash também pode ser removida de forma simultânea.
Pular lista e listas ordenadas semelhantes são usadas quando a localidade de referência é necessária. Por exemplo: localizando voos próximo e antes de uma data em um aplicativo.
Uma árvore de busca de busca binária na memória é excelente e usada com mais frequência.
Pular lista Vs Splay Tree Vs Hash Table Runtime no dicionário find op