Solução de palavras cruzadas em japonês com P̶y̶t̶h̶o̶̶n̶ Rust e WebAssembly

Logotipo da ferrugem como nonogram


Como criar um solucionador de não-programa para Python, reescreva-o no Rust para executá-lo diretamente no navegador através do WebAssembly.


TL; DR


Iniciar


Sobre palavras cruzadas em japonês (nonograms) no Habré já havia várias postagens. Exemplo
e mais um .


As imagens são criptografadas com números localizados à esquerda das linhas e também acima das colunas. O número de números mostra quantos grupos de células pretas (ou suas cores, para palavras cruzadas coloridas) estão na linha ou coluna correspondente e os próprios números - quantas células mescladas cada um desses grupos contém (por exemplo, um conjunto de três números - 4, 1 e 3 significa que nesta linha existem três grupos: o primeiro - de quatro, o segundo - de um, o terceiro - de três células negras). Em palavras cruzadas em preto e branco, os grupos devem ser separados por pelo menos uma célula vazia; nas palavras cruzadas em cores, esta regra se aplica apenas a grupos de uma cor e grupos multicoloridos podem ser espaçados (as células vazias também podem estar nas bordas das linhas). É necessário determinar a localização dos grupos de células.

Um dos pontos de vista mais geralmente aceitos é que palavras cruzadas "corretas" podem ser chamadas apenas daquelas que são resolvidas de maneira "lógica". Isso geralmente é chamado de método de solução, no qual as dependências entre diferentes linhas e / ou colunas não são levadas em consideração. Em outras palavras, uma solução é uma sequência de decisões independentes de linhas ou colunas individuais até que todas as células sejam preenchidas (mais sobre o algoritmo abaixo). Por exemplo, somente esses nonogramas podem ser encontrados no site http://nonograms.org/ ( http://nonograms.ru/ ). Os nonogramas deste site já foram citados como exemplo no artigo Resolvendo palavras cruzadas coloridas em japonês na velocidade da luz . Para fins de comparação e verificação, meu solucionador também adicionou suporte para baixar e analisar palavras cruzadas deste site (obrigado a KyberPrizrak pela permissão para usar materiais de seu site).


No entanto, o conceito de nonogramas pode ser estendido a um problema mais geral, quando o método "lógico" usual leva a um beco sem saída. Nesses casos, é preciso fazer uma suposição sobre a cor de uma célula e, depois de provar que essa cor leva a uma contradição, marcar a cor oposta para essa célula. A sequência de tais etapas pode (se você tiver paciência) nos fornecer todas as soluções. Este artigo abordará principalmente a solução de um caso mais geral de palavras cruzadas.


Python


Cerca de um ano e meio atrás, acidentalmente me deparei com um artigo que descrevia um método para resolver uma única linha (como se viu mais tarde, o método era bastante lento).


Quando implementei esse método no Python (minha principal linguagem de trabalho) e adicionei uma atualização seqüencial de todas as linhas, vi que tudo isso não estava sendo resolvido muito rapidamente. Depois de estudar o material, verificou-se que, neste tópico, existem muitos trabalhos e implementações que oferecem abordagens diferentes para esta tarefa.


Pareceu-me que o trabalho mais ambicioso na análise de várias implementações de solucionadores foi realizado por Jan Wolter, publicando em seu site (que, até onde eu sei, continua sendo o maior repositório público de nonogramas na Internet), um estudo detalhado contendo uma riqueza de informações e links que podem ajudar na criando seu próprio solucionador.


Estudando várias fontes (elas estarão no final do artigo), eu melhorei gradualmente a velocidade e a funcionalidade do meu solucionador. Como resultado, fiquei viciado e participei da implementação, refatoração e depuração de algoritmos por cerca de 10 meses em um tempo livre do trabalho.


Algoritmos principais


O solucionador resultante pode ser representado na forma de quatro níveis de decisão:


  • solucionador linear ( linha ): na entrada, uma linha de células e uma linha de descrição (pistas); na saída, uma linha parcialmente resolvida. Na solução python, implementei 4 algoritmos diferentes (3 deles são adaptados para palavras cruzadas em cores). O mais rápido acabou sendo o algoritmo BguSolver, nomeado após a fonte original . Este é um método muito eficaz e praticamente padrão para resolver seqüências de caracteres não programadas usando programação dinâmica. O pseudocódigo deste método pode ser encontrado, por exemplo, neste artigo .


  • ( propagação ) colocamos todas as linhas e colunas em uma fila, passamos por isso com um solucionador linear, quando recebemos novas informações ao resolver uma linha (coluna), atualizamos a fila, respectivamente, com novas colunas (linhas). Continue até a linha estar vazia.


    Exemplo e código

    Tomamos a próxima tarefa a resolver a partir da fila. Seja uma sequência vazia (não resolvida) de comprimento 7 (que a denotamos como ??????? ) com uma descrição dos blocos [2, 3] . O solucionador linear produzirá uma string parcialmente resolvida ?X??XX? onde X é a célula preenchida. Ao atualizar a linha, vemos que as colunas com os números 1, 4, 5 foram alteradas (a indexação começa em 0). Isso significa que novas informações apareceram nas colunas indicadas e podem ser retornadas ao solucionador "linear". Colocamos essas colunas na fila de tarefas com maior prioridade (a fim de fornecê-las ao solucionador linear a seguir).


     def propagation(board): line_jobs = PriorityDict() for row_index in range(board.height): new_job = (False, row_index) line_jobs[new_job] = 0 for column_index in range(board.width): new_job = (True, column_index) line_jobs[new_job] = 0 for (is_column, index), priority in line_jobs.sorted_iter(): new_jobs = solve_and_update(board, index, is_column) for new_job in new_jobs: # upgrade priority new_priority = priority - 1 line_jobs[new_job] = new_priority def solve_and_update(board, index, is_column): if is_column: row_desc = board.columns_descriptions[index] row = tuple(board.get_column(index)) else: row_desc = board.rows_descriptions[index] row = tuple(board.get_row(index)) updated = line_solver(row_desc, row) if row != updated: for i, (pre, post) in enumerate(zip(row, updated)): if _is_pixel_updated(pre, post): yield (not is_column, i) if is_column: board.set_column(index, updated) else: board.set_row(index, updated) 



  • ( análise ) para cada célula não resolvida, classificamos todas as opções de cores e tentamos a propagação com essas novas informações. Se tivermos uma contradição, jogaremos essa cor fora das opções de cores da célula e tentaremos nos beneficiar dela novamente usando propagação. Se for resolvido até o fim, adicionamos a solução à lista de soluções, mas continuamos os experimentos com outras cores (pode haver várias soluções). Se chegarmos a uma situação em que é impossível resolver ainda mais, simplesmente ignoramos e repetimos o procedimento com uma cor / célula diferente.


    Código

    Retorna True se uma contradição for recebida como resultado da amostra.


     def probe(self, cell_state): board = self.board pos, assumption = cell_state.position, cell_state.color # already solved if board.is_cell_solved(pos): return False if assumption not in board.cell_colors(pos): LOG.warning("The probe is useless: color '%s' already unset", assumption) return False save = board.make_snapshot() try: board.set_color(cell_state) propagation( board, row_indexes=(cell_state.row_index,), column_indexes=(cell_state.column_index,)) except NonogramError: LOG.debug('Contradiction', exc_info=True) # rollback solved cells board.restore(save) else: if board.is_solved_full: self._add_solution() board.restore(save) return False LOG.info('Found contradiction at (%i, %i)', *pos) try: board.unset_color(cell_state) except ValueError as ex: raise NonogramError(str(ex)) propagation( board, row_indexes=(pos.row_index,), column_indexes=(pos.column_index,)) return True 



  • ( retorno ) se, durante a investigação, você não ignora um quebra-cabeça parcialmente resolvido, mas continua invocando recursivamente o mesmo procedimento, obtemos retorno (em outras palavras, uma caminhada completa na profundidade da árvore de decisão em potencial). Aqui, um grande papel começa a desempenhar, qual das células será escolhida como a próxima extensão da solução em potencial. Uma boa pesquisa sobre este tópico está nesta publicação .


    Código

    O retorno é bastante confuso comigo, mas essas duas funções descrevem aproximadamente o que acontece durante uma pesquisa recursiva


     def search(self, search_directions, path=()): """ Return False if the given path is a dead end (no solutions can be found) """ board = self.board depth = len(path) save = board.make_snapshot() try: while search_directions: state = search_directions.popleft() assumption, pos = state.color, state.position cell_colors = board.cell_colors(pos) if assumption not in cell_colors: LOG.warning("The assumption '%s' is already expired. " "Possible colors for %s are %s", assumption, pos, cell_colors) continue if len(cell_colors) == 1: LOG.warning('Only one color for cell %r left: %s. Solve it unconditionally', pos, assumption) try: self._solve_without_search() except NonogramError: LOG.warning( "The last possible color '%s' for the cell '%s' " "lead to the contradiction. " "The path %s is invalid", assumption, pos, path) return False if board.is_solved_full: self._add_solution() LOG.warning( "The only color '%s' for the cell '%s' lead to full solution. " "No need to traverse the path %s anymore", assumption, pos, path) return True continue rate = board.solution_rate guess_save = board.make_snapshot() try: LOG.warning('Trying state: %s (depth=%d, rate=%.4f, previous=%s)', state, depth, rate, path) success = self._try_state(state, path) finally: board.restore(guess_save) if not success: try: LOG.warning( "Unset the color %s for cell '%s'. Solve it unconditionally", assumption, pos) board.unset_color(state) self._solve_without_search() except ValueError: LOG.warning( "The last possible color '%s' for the cell '%s' " "lead to the contradiction. " "The whole branch (depth=%d) is invalid. ", assumption, pos, depth) return False if board.is_solved_full: self._add_solution() LOG.warning( "The negation of color '%s' for the cell '%s' lead to full solution. " "No need to traverse the path %s anymore", assumption, pos, path) return True finally: # do not restore the solved cells on a root path - they are really solved! if path: board.restore(save) return True def _try_state(self, state, path): board = self.board full_path = path + (state,) probe_jobs = self._get_all_unsolved_jobs(board) try: # update with more prioritized cells for new_job, priority in self._set_guess(state): probe_jobs[new_job] = priority __, best_candidates = self._solve_jobs(probe_jobs) except NonogramError as ex: LOG.warning('Dead end found (%s): %s', full_path[-1], str(ex)) return False rate = board.solution_rate LOG.info('Reached rate %.4f on %s path', rate, full_path) if rate == 1: return True cells_left = round((1 - rate) * board.width * board.height) LOG.info('Unsolved cells left: %d', cells_left) if best_candidates: return self.search(best_candidates, path=full_path) return True 



Portanto, começamos a resolver nossas palavras cruzadas a partir do segundo nível (o primeiro é adequado apenas para o caso degenerado, quando em todas as palavras cruzadas há apenas uma linha ou coluna) e subimos gradualmente os níveis. Como você pode imaginar, cada nível causa o nível subjacente várias vezes; portanto, para uma solução eficaz, é imperativo ter primeiro e segundo níveis rápidos, que podem ser chamados milhões de vezes para quebra-cabeças complexos.


Nesse estágio, verifica-se (bastante esperado) que o python não seja a ferramenta adequada para o desempenho máximo em uma tarefa que exige muita CPU: todos os cálculos são extremamente ineficientes em comparação com as linguagens de nível inferior. Por exemplo, o solucionador de BGU com algoritmos mais próximos (em Java), de acordo com os resultados das medições, mostrou ser 7-17 (às vezes até 27) vezes mais rápido em uma variedade de tarefas.


Mais detalhes
         pynogram_my BGU_my speedup
 Dançarina 0.976 0.141 6.921986      
 Cat 1.064 0.110 9.672727      
 Skid 1.084 0.101 10.732673     
 Bucks 1.116 0.118 9.457627      
 Edge 1.208 0,094 12,851064     
 Fumaça 1.464 0.120 12.200000     
 Nó 1.332 0.140 9.514286      
 Balanço 1,784 0,138 12,927536     
 Mãe 2,108 0,147 14,340136     
 DiCap 2.076 0,176 11,795455     
 Tragic 2,386 0,265 8,935849      
 Merka 2.084 0,196 10.632653     
 Petro 2.948 0.219 13.461187     
 M&M 3,588 0,375 9,568000      
 Assinado 4.068 0,242 16.809917     
 Luz 3.848 0.488 7.885246      
 Forever 111.000 13.570 8.179808  
 Center 5.700 0,327 17,431193     
 Hot 3.150 0.278 11.330935     
 Karatê 2.500 0.219 11.415525     
 9-Dom 510.000 70.416 7.242672      
 Bandeira 149.000 5.628 26.474769     
 Leão 71.000 2.895 24.525043     
 Marley 12.108 4.405 2.748695      
 Coisa 321.000 46.166 6.953169      
 Natureza inf 433.138 inf     
 Sierp inf inf NaN      
 Gettys inf inf NaN      

As medições foram realizadas no meu carro, os quebra-cabeças são retirados do conjunto padrão que Jan Wolter usou em sua comparação


E isso já foi depois que eu comecei a usar o PyPy, e no CPython padrão o tempo de cálculo foi maior do que no PyPy 4-5 vezes! Podemos dizer que o desempenho de um solucionador Java semelhante acabou sendo 28-85 vezes maior que o código CPython.


Tentativas de melhorar o desempenho do meu solucionador usando a criação de perfil (cProfile, SnakeViz, line_profiler) levaram a alguma aceleração, mas é claro que elas não deram um resultado estranho.


Resumo :


O + solver pode resolver todos os quebra-cabeças dos sites https://webpbn.com , http://nonograms.org e seu próprio formato (baseado em ini)


+ resolve nonogramas em preto e branco e em cores com qualquer número de cores (o número máximo de cores encontradas é 10)


+ resolve quebra-cabeças com tamanhos de bloco ausentes (manchados). Um exemplo de um quebra-cabeça .


+ pode renderizar quebra-cabeças no console / nas maldições / janela no navegador (ao instalar a opção adicional pynogram-web ). Para todos os modos, é possível visualizar o progresso da solução em tempo real.


- cálculos lentos (em comparação com as implementações descritas na comparação de artigos dos solucionadores, consulte a tabela ).


- retorno ineficiente: alguns quebra-cabeças podem ser resolvidos por horas (quando a árvore de decisão é muito grande).


Ferrugem


No começo do ano, comecei a estudar Rust. Comecei, como sempre, com o livro , aprendi sobre o WASM, passei pelo tutorial proposto . No entanto, eu queria uma tarefa real na qual você possa se engajar nos pontos fortes da linguagem (principalmente sua super performance), e não alguns exemplos inventados por alguém. Então voltei aos nonogramas. Mas agora eu já tinha uma versão funcional de todos os algoritmos em Python, que foi deixada para "apenas" reescrever.


As boas notícias estavam me esperando desde o início: descobriu-se que Rust, com seu sistema de tipos, descreve perfeitamente as estruturas de dados para minha tarefa. Por exemplo, uma das correspondências básicas BinaryColor + BinaryBlock / MultiColor + ColoredBlock permite separar permanentemente os nonogramas em preto e branco e em cores. Se em algum lugar do código tentarmos resolver uma sequência colorida usando blocos de descrição binária comuns, obteremos um erro de compilação sobre incompatibilidade de tipos.


Tipos básicos se parecem com isso
 pub trait Color { fn blank() -> Self; fn is_solved(&self) -> bool; fn solution_rate(&self) -> f64; fn is_updated_with(&self, new: &Self) -> Result<bool, String>; fn variants(&self) -> Vec<Self>; fn as_color_id(&self) -> Option<ColorId>; fn from_color_ids(ids: &[ColorId]) -> Self; } pub trait Block { type Color: Color; fn from_str_and_color(s: &str, color: Option<ColorId>) -> Self { let size = s.parse::<usize>().expect("Non-integer block size given"); Self::from_size_and_color(size, color) } fn from_size_and_color(size: usize, color: Option<ColorId>) -> Self; fn size(&self) -> usize; fn color(&self) -> Self::Color; } #[derive(Debug, PartialEq, Eq, Hash, Clone)] pub struct Description<T: Block> where T: Block, { pub vec: Vec<T>, } // for black-and-white puzzles #[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, PartialOrd)] pub enum BinaryColor { Undefined, White, Black, BlackOrWhite, } impl Color for BinaryColor { // omitted } #[derive(Debug, PartialEq, Eq, Hash, Default, Clone)] pub struct BinaryBlock(pub usize); impl Block for BinaryBlock { type Color = BinaryColor; // omitted } // for multicolor puzzles #[derive(Debug, PartialEq, Eq, Hash, Default, Copy, Clone, PartialOrd, Ord)] pub struct MultiColor(pub ColorId); impl Color for MultiColor { // omitted } #[derive(Debug, PartialEq, Eq, Hash, Default, Clone)] pub struct ColoredBlock { size: usize, color: ColorId, } impl Block for ColoredBlock { type Color = MultiColor; // omitted } 

Ao portar o código, alguns pontos indicaram claramente que uma linguagem de tipo estaticamente como Rust (bem, ou, por exemplo, C ++) é mais adequada para esta tarefa. Mais precisamente, genéricos e características descrevem um domínio melhor do que hierarquias de classe. Portanto, no código Python, eu tinha duas classes para um BguSolver linear, BguSolver e BguColoredSolver que resolviam uma linha em preto e branco e uma linha de cores, respectivamente. No código Rust, ainda tenho a única struct DynamicSolver<B: Block, S = <B as Block>::Color> genérica struct DynamicSolver<B: Block, S = <B as Block>::Color> , que pode resolver os dois tipos de tarefas, dependendo do tipo passado durante a criação ( DynamicSolver<BinaryBlock>, DynamicSolver<ColoredBlock> ). Isso, é claro, não significa que algo semelhante não possa ser feito em Python, apenas no Rust o sistema de tipos claramente indicado para mim: se você não fosse por esse caminho, teria que escrever uma tonelada de código repetido.


Além disso, quem tentou escrever no Rust, sem dúvida percebeu o efeito da "confiança" no compilador, quando o processo de escrever código é reduzido ao seguinte algoritmo pseudo-meta:


 write_initial_code
 while (compiler_hints = $ (verificação de carga))! = 0;  fazer
     fix_errors (dicas do compilador)
 fim

Quando o compilador parar de emitir erros e avisos, seu código será consistente com o sistema de tipos e com o verificador de empréstimos e você avisará antecipadamente a ocorrência de vários bugs em potencial (é claro, sujeitos a um design cuidadoso dos tipos de dados).


Vou dar alguns exemplos de funções que mostram como o código Rust pode ser conciso (em comparação com os equivalentes do Python).


unsolved_neighbours

Fornece uma lista de "vizinhos" não resolvidos para um determinado ponto (x, y)


 def unsolved_neighbours(self, position): for pos in self.neighbours(position): if not self.is_cell_solved(*pos): yield pos 

 fn unsolved_neighbours(&self, point: &Point) -> impl Iterator<Item = Point> + '_ { self.neighbours(&point) .into_iter() .filter(move |n| !self.cell(n).is_solved()) } 

parcial_sums

Para um conjunto de blocos descrevendo uma linha, forneça somas parciais (levando em consideração as lacunas necessárias entre os blocos) Os índices resultantes indicarão a posição mínima na qual o bloco pode terminar (essas informações serão usadas posteriormente para um solucionador linear).


Por exemplo, para esse conjunto de blocos [2, 3, 1] , temos a saída [2, 6, 8] , o que significa que o primeiro bloco pode ser deslocado ao máximo para a esquerda, de modo que sua borda direita ocupe a 2ª célula, da mesma forma que o resto blocos:


             1 2 3 4 5 6 7 8 9 
             _ _ _ _ _ _ _ _ _ _
      2 3 1 | _ | _ | _ | _ | _ | _ | _ | _ | _ | 
               ^ ^ ^
               |  |  |
 fim de 1 bloco |  |  | 
 fim do bloco 2 -------- |
 fim do bloco 3 ------------

 @expand_generator def partial_sums(blocks): if not blocks: return sum_so_far = blocks[0] yield sum_so_far for block in blocks[1:]: sum_so_far += block + 1 yield sum_so_far 

 fn partial_sums(desc: &[Self]) -> Vec<usize> { desc.iter() .scan(None, |prev, block| { let current = if let Some(ref prev_size) = prev { prev_size + block.0 + 1 } else { block.0 }; *prev = Some(current); *prev }) .collect() } 

Ao portar, fiz várias alterações


  • o núcleo do solucionador (algoritmos) passou por pequenas alterações (principalmente para suportar tipos genéricos de células e blocos)
  • deixou o único algoritmo (mais rápido) para o solucionador linear
  • em vez do formato ini, introduziu um formato TOML ligeiramente modificado
  • não adicionou suporte para palavras cruzadas borradas, porque, estritamente falando, esta é uma classe de tarefas diferente
  • deixou o único caminho para a saída - apenas para o console, mas agora as células coloridas no console são desenhadas realmente coloridas (graças a esta caixa )


    assim

    Jack pardal




Ferramentas úteis


  • O clippy é um analisador estático padrão que às vezes pode até dar dicas, aumentando ligeiramente o desempenho do código.
  • O valgrind é uma ferramenta para análise dinâmica de aplicativos. Usei-o como um criador de perfil para procurar botneks ( valrgind --tool=callgrind ) e, particularmente, seções de código valrgind --tool=massif memória ( valrgind --tool=massif ). Dica: defina [profile.release] debug = true para Cargo.toml antes de iniciar o criador de perfil. Isso deixará os caracteres de depuração no executável.
  • kcachegrind para visualizar arquivos de callgrind. Uma ferramenta muito útil para encontrar os lugares mais problemáticos em termos de desempenho.

Desempenho


Que para que reescrita em Rust foi iniciada. Pegamos as palavras cruzadas da tabela de comparação já mencionada e as executamos nos melhores solucionadores descritos no artigo original. Resultados e descrição das execuções aqui . Pegamos o arquivo resultante e construímos alguns gráficos: como o tempo de solução varia de milissegundos a dezenas de minutos, o gráfico é feito com uma escala logarítmica.


executar no laptop jupyter
 import pandas as pd import numpy as np import matplotlib.pyplot as plt %matplotlib inline # strip the spaces df = pd.read_csv('perf.csv', skipinitialspace=True) df.columns = df.columns.str.strip() df['name'] = df['name'].str.strip() # convert to numeric df = df.replace('\+\ *', np.inf, regex=True) ALL_SOLVERS = list(df.columns[3:]) df.loc[:,ALL_SOLVERS] = df.loc[:,ALL_SOLVERS].apply(pd.to_numeric) # it cannot be a total zero df = df.replace(0, 0.001) #df.set_index('name', inplace=True) SURVEY_SOLVERS = [s for s in ALL_SOLVERS if not s.endswith('_my')] MY_MACHINE_SOLVERS = [s for s in ALL_SOLVERS if s.endswith('_my') and s[:-3] in SURVEY_SOLVERS] MY_SOLVERS = [s for s in ALL_SOLVERS if s.endswith('_my') and s[:-3] not in SURVEY_SOLVERS] bar_width = 0.17 df_compare = df.replace(np.inf, 10000, regex=True) plt.rcParams.update({'font.size': 20}) def compare(first, others): bars = [first] + list(others) index = np.arange(len(df)) fig, ax = plt.subplots(figsize=(30,10)) df_compare.sort_values(first, inplace=True) for i, column in enumerate(bars): ax.bar(index + bar_width*i, df_compare[column], bar_width, label=column[:-3]) ax.set_xlabel("puzzles") ax.set_ylabel("Time, s (log)") ax.set_title("Compare '{}' with others (lower is better)".format(first[:-3])) ax.set_xticks(index + bar_width / 2) ax.set_xticklabels("#" + df_compare['ID'].astype(str) + ": " + df_compare['name'].astype(str)) ax.legend() plt.yscale('log') plt.xticks(rotation=90) plt.show() fig.savefig(first[:-3] + '.png', bbox_inches='tight') for my in MY_SOLVERS: compare(my, MY_MACHINE_SOLVERS) compare(MY_SOLVERS[0], MY_SOLVERS[1:]) 

solucionador de python

desempenho do pynogram

(a imagem é clicável )


Vemos que o picograma aqui é mais lento que todos os solucionadores apresentados. A única exceção a essa regra é o solucionador Tamura / Copris baseado no SAT, que resolve os quebra-cabeças mais simples (a parte esquerda do gráfico) por mais tempo que o nosso. No entanto, esse é um recurso dos solucionadores SAT: eles são projetados para palavras cruzadas super complexas nas quais um solucionador regular fica preso no retorno por um longo tempo. Isso é claramente visível no lado direito do gráfico, onde Tamura / Copris resolve os quebra-cabeças mais difíceis dezenas e centenas de vezes mais rápido que todos os outros.


solucionador de ferrugem

desempenho sem grade

(a imagem é clicável )


Este gráfico mostra que a não grade em tarefas simples também lida com ou um pouco pior que os solucionadores de alto desempenho escritos em C e C ++ ( Wolter e Syromolotov ). Com a complicação das tarefas, nosso solucionador repete aproximadamente a trajetória do solucionador de BGU (Java), mas quase sempre à frente dele em cerca de uma ordem de magnitude. Nas tarefas mais difíceis, Tamura / Copris está sempre à frente de todos.


ferrugem vs python

py-vs-rust-performance

(a imagem é clicável )


E, finalmente, uma comparação dos nossos dois solucionadores descritos aqui. Pode-se ver que o solucionador Rust quase sempre está de 1 a 3 ordens de magnitude à frente do solucionador python.


Resumo :


O + solver pode resolver todos os quebra-cabeças dos sites https://webpbn.com (exceto blotted - com tamanhos de blocos parcialmente ocultos), http://nonograms.org e seu próprio formato (baseado em TOML)


+ resolve nonogramas em preto e branco e em cores com qualquer número de cores


+ pode renderizar quebra-cabeças no console (cor c webpbn.com desenha cores reais)


+ funciona rápido (em comparação com as implementações descritas na comparação de artigos dos solucionadores, consulte a tabela).


- o backtracking permaneceu ineficaz, como na solução Python: alguns quebra-cabeças (por exemplo , um 20x20 tão inofensivo ) podem ser resolvidos por horas (quando a árvore de decisão é muito grande). Talvez, em vez de voltar atrás, valha a pena usar os solucionadores SAT já mencionados no hub É verdade que o único solucionador de SAT que encontrei no Rust à primeira vista parece inacabado e abandonado.


Webassembly


Portanto, reescrever o código no Rust valeu a pena: o solucionador se tornou muito mais rápido. No entanto, o Rust nos oferece outro recurso incrivelmente interessante: compilação no WebAssembly e a capacidade de executar seu código diretamente no navegador.


Para implementar esse recurso, existe uma ferramenta especial para o Rust que fornece os ligantes necessários e gera um padrão para você executar sem problemas as funções do Rust no código JS - este wasm-pack (+ wasm-bindgen ). A maior parte do trabalho com ele e outras ferramentas importantes já está descrita no tutorial Rust and WebAssembly . No entanto, há alguns pontos que eu tive que descobrir por conta própria:


  • ao ler, cria a sensação de que o tutorial foi escrito principalmente para um desenvolvedor de JS que deseja acelerar seu código com o Rust. Bem, ou pelo menos para alguém familiarizado com o npm . Para mim, como uma pessoa distante do front-end, foi surpreendente descobrir que mesmo o exemplo padrão do livro não deseja trabalhar com um servidor da Web de terceiros diferente do npm run start .


    Felizmente, o wasm-pack possui um modo que permite gerar código JS regular (que não é um módulo npm). wasm-pack build --target no-modules --no-typescript na saída produzirá apenas dois arquivos: project-name.wasm - o binário do código Rust compilado no WebAssembly e project-name.js . O último arquivo pode ser incluído em qualquer página HTML <script src="project-name.js"></script> e usar as funções WASM sem se preocupar com npm, webpack, ES6, módulos e outras alegrias do desenvolvedor JS moderno. O modo no-modules é ideal para desenvolvedores não front-end durante o desenvolvimento de um aplicativo WASM, bem como para exemplos e demonstrações, porque não requer nenhuma infraestrutura front-end adicional.


  • O WebAssembly é bom para tarefas muito pesadas para JavaScript. Antes de tudo, são tarefas que realizam muitos cálculos. E, nesse caso, essa tarefa pode ser executada por um longo tempo, mesmo com o WebAssembly, violando o princípio assíncrono da web moderna. Estou falando de todos os tipos de scripts de aviso: sem resposta que eu observei quando meu solucionador estava funcionando. Para resolver esse problema, você pode usar o mecanismo de trabalho da Web . Nesse caso, o esquema para trabalhar com funções WASM "pesadas" pode ser assim:


    1. no script principal de um evento (por exemplo, clicando em um botão), envie uma mensagem ao trabalhador com a tarefa de iniciar uma função pesada.
    2. , .
    3. - ()


WASM- JS, WASM . - ( ), HashMap , . ( JS) , / .


, Mutex , thread-safe. smart- . thread-safe Rc Arc RefCell RwLock . : 30%. --features=threaded thread-safe , WASM-.


6574 8098 ( 10 ):


idnon-thread-safethread-safeweb-interface
65745.47.49.2
809821.528.429.9

, - 40..70% , , (32..37%) thread-safe ( cargo build --release --features=threaded ).


Firefox 67.0 Chromium 74.0.


WASM- ( ). https://webpbn.com/ http://www.nonograms.org/


TODO


  • "" /, / .


  • , . "" , , . .


  • ( , 3600 ). WASM , ( , (!) , , WASM). , , - , nonogrid .


  • . : , , WASM . , ( ) , JS , .


  • JS. backtrace, .


  • ( TOML- )



Sumário


  • , (, , etc).


  • Rust 1-3 PyPy 1.5-2 ( ).


  • Python Rust , Python (, , comprehensions), Rust-.


  • Rust WebAssembly . Rust , WASM, ( 1.5 ).




  1. The 'pbnsolve' Paint-by-Number Puzzle Solver by Jan Wolter and the survey
  2. The BGU Nonograms Project
  3. Solving Nonograms by combining relaxations
  4. An Efficient Approach to Solving Nonograms
  5. Color and black and white Japanese crosswords on-line
  6. 'Nonolib' library by Dr. Steven Simpson
  7. Rust and WebAssembly

Source: https://habr.com/ru/post/pt454586/


All Articles