Ignorar lista x árvore de pesquisa binária


Respostas:


257

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.

  • Bloquear listas de pular é incrivelmente rápido. Eles escalam incrivelmente bem com o número de acessos simultâneos. É isso que torna as listas de pulos especiais, outras estruturas de dados baseadas em bloqueio tendem a coaxar sob pressão.
  • As listas de pular sem bloqueio são consistentemente mais rápidas do que as listas de pular, mas apenas por pouco.
  • as listas de pulos transacionais são consistentemente 2-3 vezes mais lentas que as versões de bloqueio e não bloqueio.
  • trancar árvores vermelho-preto coaxar sob acesso simultâneo. Seu desempenho diminui linearmente com cada novo usuário simultâneo. Das duas implementações de árvore de bloqueio vermelho-preto conhecidas, uma tem essencialmente um bloqueio global durante o reequilíbrio da árvore. O outro usa escalada de bloqueio sofisticada (e complicada), mas ainda não executa significativamente a versão de bloqueio global.
  • árvores preto-vermelho sem bloqueio não existem (não é mais verdade, consulte Atualização).
  • as árvores preto-vermelho transacionais são comparáveis ​​com as listas de ignorados transacionais. Isso foi muito surpreendente e muito promissor. Memória transacional, embora mais lenta, se bem mais fácil de escrever. Pode ser tão fácil quanto pesquisar e substituir rapidamente na versão não simultânea.

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.


3
Sem mencionar que em um skiplist não degenerado, cerca de 50% dos nós devem ter apenas um único link, o que torna a inserção e a exclusão extremamente eficientes.
Adisak

2
O reequilíbrio não requer um bloqueio mutex. Veja cl.cam.ac.uk/research/srg/netos/lock-free
Jon Harrop

3
@ Jon, sim e não. Não há implementações de árvore vermelho-preto sem bloqueio conhecidas. Fraser e Harris mostram como uma árvore vermelho-preta baseada em memória transacional é implementada e seu desempenho. A memória transacional ainda está muito presente na arena de pesquisa; portanto, no código de produção, uma árvore vermelha-preta ainda precisará bloquear grandes porções da árvore.
Deft_code 21/05

4
@deft_code: A Intel anunciou recentemente uma implementação da Memória Transacional via TSX em Haswell. Isso pode ser interessante por aquelas estruturas de dados livres de bloqueio que você mencionou.
Mike Bailey

2
Acho que a resposta de Fizz está mais atualizada (a partir de 2015) do que esta (2012) e, portanto, provavelmente deve ser a resposta preferida até agora.
FNL

81

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 .

insira a descrição da imagem aqui

"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.


12

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.


1
não é um percurso em ordem para uma árvore de bin tão simples como: "def func (nó): func (esquerda (nó)); op (nó); func (direita (nó))"?
Claudiu

6
Claro, isso é verdade se você deseja atravessar tudo em uma chamada de função. mas fica muito mais irritante se você quiser ter uma travessia no estilo do iterador, como no std :: map.
Evan Teran

@ Evan: Não está em uma linguagem funcional onde você pode apenas escrever no 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;:? =). 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.
Claudiu

1
@Evan: sim, funciona desde que o parâmetro do nó seja cortado da árvore durante uma modificação. A travessia C ++ tem a mesma restrição.
Deft_code 18/11/10

10

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.


4
Eu acho que você não deve chamar árvore binária um B-Tree, há um completamente diferentes DS com esse nome
Shihab Shahriar Khan

8

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.


isso seria um motivo para não usar a lista de pulos.
Claudiu

7
citando o MSDN, "As chances [de 100 elementos de nível 1] são precisamente 1 em 1.267.650.600.228.229.401.496.703.205.376".
peterchen 02/11/08

8
Por que você diria que eles usam menos memória?
Jonathan

1
@ Peterchen: Entendo, obrigado. Portanto, isso não ocorre com as listas de pulos deterministas? @ Mitch: "Skip Lists use less memory". Como as listas de pulos usam menos memória que as árvores binárias balanceadas? Parece que eles têm 4 ponteiros em cada nó e nós duplicados, enquanto as árvores têm apenas 2 ponteiros e nenhuma duplicata.
Jon Harrop 21/05

1
@ Jon Harrop: Os nós no nível um precisam apenas de um ponteiro por nó. Quaisquer nós em níveis mais altos precisam apenas de dois ponteiros por nó (um para o próximo nó e um para o nível abaixo dele), embora, obviamente, um nó de nível 3 signifique que você está usando um total de 5 ponteiros para esse valor. Obviamente, isso ainda consumirá muita memória (mais do que uma pesquisa binária, se você quiser uma lista de ignorados inútil e tiver um grande conjunto de dados) ... mas acho que estou perdendo alguma coisa ...
Brian

2

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 :-)


5
"não existem soluções livres de bloqueio que usem diretamente o CAS apenas para qualquer estrutura de dados O (logn)". Não é verdade. Para exemplos de contadores, consulte cl.cam.ac.uk/research/srg/netos/lock-free
Jon Harrop

-1

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


Dei uma olhada rápida e seus resultados parecem mostrar o SkipList mais rápido que o SplayTree.
Chinasaur 31/08/13

É enganoso assumir a randomização como parte da lista de ignorados. Como os elementos são ignorados é crucial. A randomização é adicionada para estruturas probabilísticas.
User568109
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.