Você está na página 1de 8

Universidade Federal de Pernambuco

Centro de Informática
Processamento de Cadeias e Caracteres
Relatório do Projeto 1
Aluno: Arnaldo Rafael Morais Andrade Data: 21/10/2018
Marvson Allan Pontes de Assis

1) Identificação
Nosso grupo inclui ​Arnaldo Rafael Morais Andrade ​e ​Marvson Allan Pontes de Assis​​.
Arnaldo contribuiu para a implementação do algoritmo do Sellers, para a leitura de texto e para a
execução dos testes. Marvson contribuiu para a implementação dos algoritmos Aho-Corasick,
Wu-Manber, Boyer-Moore e Shift-Or.

2) Implementação
2.1) Funcionamento da Ferramenta
Os algoritmos implementados foram: Boyer-Moore, Aho-Corasick e ShiftOr para
casamento exato, sendo o primeiro deles para busca sequencial e o segundo para busca
paralela; Sellers e o algoritmo de Wu Manber, para casamento aproximado, sendo o primeiro
para padrões de tamanho acima de BITSET_SIZE caracteres e o segundo para padrões de
tamanho abaixo de BITSET_SIZE caracteres. BITSET_SIZE é declarado no arquivo
“config.h” e é configurável em tempo de compilação.
A estratégia para os algoritmos é a seguinte: Caso exista apenas um padrão de
tamanho menor que 100 e busca exata, usamos o Shift-Or. Caso seja um padrão com mais de
100 caracteres e busca exata, usamos o Boyer-Moore. Caso seja apenas um padrão com no
máximo 100 caracteres e busca aproximada, usamos o Wu-Manber. Caso seja apenas um
padrão com mais de 100 caracteres e busca aproximada, usamos o Sellers. Caso sejam vários
padrões e busca exata, usamos o Aho-Corasick.
2.2) Detalhes Relevantes
a. Estratégias de Leitura
A leitura foi feita linha por linha, não foram feitas otimizações adicionais aqui.
b. Convenções
Usamos namespaces em vez de classes para ficar menos complexo e não precisar ficar
instanciando classes. Qualquer estado é mantido pelo pmt.
Escolhemos usar o namespace completo sempre e evitar o uso de “using namespace = …”,
exceto quando isso prejudica a leitura. Nesse caso, em vez de evitarmos de usar namespaces,
reduzimos o nome do tipo da variável, como descrito na seção 2.2 e.
c. Wu-Manber
Implementamos o Wu-Manber antes do Shift-Or, principalmente porque não iríamos utilizar o
Shift-Or, mas por fim reaproveitamos a função make_mask do Wu-Manber para o Shift-Or.
Inicialmente, estávamos com a ideia ambiciosa de fazer o Wu-Manber funcionar para padrões de
tamanho definido em tempo de execução. Basicamente isso significa que poderíamos usar cadeias de
bits de tamanho arbitrário. Para tal, usaríamos a biblioteca boost, especificamente, a estrutura
boost::dynamic_bitset. Em teoria, essa é uma escolha muito boa. A parte específica da biblioteca
boost que implementa o template dynamic_bitset é razoavelmente leve (21 Mb), é composta apenas
de arquivos de cabeçário. O problema veio quando começamos a testar e percebemos um tempo
superior a 5 segundos. Após comentar as operações de shif e de or, o tempo caia. O dynamic bitset
em teoria é uma estrutura muito boa, mas na prática, o compilador não tem como ter certeza se
operações como shift e or não terão efeitos colaterais complicados e não consegue otimizar essa parte
do código, resultando num código muito mais lento.
Modificamos um pouco os objetivos então e usamos um tamanho fixo de bits, entretanto não
usamos algo como um long int ou algo do tipo, e sim a estrutura std::bitset. Ao contrário do dynamic
bitset, o bitset não teve acréscimo substancial de tempo. Testamos para um BITSET_SIZE de 4
(tamanho do padrão que estávamos testando), de 10, de 20, de 100, de 200, e de 999999. Para um
BITSET_SIZE muito grande, novamente há uma lentidão absurda. Então ficamos com um
BITSET_SIZE de 200. Poderia ser maior, mas achamos que 200 é razoável. A máscara de bits do
alfabeto implementamos usando um map de caracteres para bitset’s. Para acelerar a execução, em vez
de chamarmos toda vez para cada linha o make_mask, colocamos para a máscara ser criada por fora
(em pmt) e passada por parâmetro para o wu_manber.
Outro detalhe interessante é que ao trocarmos de dynamic_bitset para bitset algo aconteceu.
Mas antes é preciso entender como o bitset armazena os dados. Cada bit é armazenado da direita para
a esquerda do bitset, de modo que ao imprimirmos o bitset, bitset[0] equivale à última posição, e
bitset[k], para um bitset de tamanho m, equivale ao (m-k)-ésimo bit.
O dynamic bitset foi construído para funcionar com essa forma de armazenamento, e portanto
em vez de usar um shift-left, usamos um shift-right. Mas quando trocamos para o bitset, os bits à
‘esquerda’ (quando imprimimos na tela) agora faziam parte do bitset, e ao dar um shift-right, eles
eram empurrados para nossa área de operação (que de fato representa o padrão). O problema disso é
que muitas vezes precisamos inicializar o bitset com valores 1, e se fizermos isso normalmente, então
todas as posições à esquerda serão 1 também. Logo, ao dar um shift-right, não é um zero que ficará à
esquerda do padrão (quando imprimimos na tela), mas um número 1. Para resolver isso de forma
eficiente, fizemos uma outra máscara que é preenchida com 1’s da posição 0 até o tamanho do
padrão, e preenchida com zeros em todo o resto. Dessa forma, apenas utilizamos uma operação de
and com essa máscara para resolver o problema.

d. Shift-Or
Não houveram muitas novidades em relação ao Wu Manber, apenas copiamos o make_mask e
modificamos algumas partes do loop interno para se adequar ao Shift-Or. Todas as considerações
sobre bitset valem aqui também. Fizemos do mais difícil para o mais fácil e isso fez com que
literalmente implementássemos o Shift-Or em dez minutos.

e. Aho-Corasick
Para o Aho-Corasick seguimos a risca a implementação das notas de aula e usamos a
convenção usada de nomenclatura usada na versão em python construída nas aulas. Utilizamos tuplas
para retorno múltiplo e a função std::tie para “desempacotar” esse retorno. Usamos STL em tudo, o
que possibilitou praticamente a construção de tudo quase exatamente como escrito no pseudo-código.
Se tivéssemos usado c++11 seria apelar ainda mais e provavelmente teríamos algo próximo de
python. A construção da máquina de estados é dada em pmt.cpp e a mesma é passada por parâmetro
pro Aho-Corasick, dessa forma a contrução da máquina de estados é feita somente uma única vez.
Usamos alguns typedefs com a nova sintaxe (using NEW_TYPE = TYPE) para facilitar a leitura.
f. Boyer-Moore
Para o algoritmo de Boyer-Moore, não houveram modificações substanciais em relação ao
pseudo-código. A regra do bom sufixo e a regra do mau caractere são executadas em pmt apenas uma
vez, para acelerar a execução.
g. Sellers
Assim como o algoritmo de Boyer-Moore, não houve modificações relevantes em relação ao
pseudo-código.
2.3) Limitações
As únicas limitações notáveis são a respeito do tamanho máximo do padrão tanto no shift-or
quanto no wu-manber, como explicado na seção 2.1.

3) Testes e Resultados
3.1) Metodologia
Variamos o tamanho do padrão, do texto e o número de padrões. Para comparação, usamos os
dados presentes no repositório SMART (​https://github.com/smart-tool/smart/tree/master/data​)
especificamente os textos em inglês e o arquivo que descreve o D.N.A. de uma bactéria E. Coli. As
comparações foram feitas manualmente e executadas com um arquivo shell unix.
Para os algoritmos não paralelos, os testes foram feitos mantendo o mesmo texto (i.e para
cada texto) e variando o tamanho do padrão.
Já para o algoritmo Aho-Corasick, a estratégia, além de manter o texto, foi de utilizar um
dicionário como o arquivo de padrões, variando o seu tamanho. Para o texto em inglês foi usado um
simples dicionário da língua, já para o D.N.A, utilizou-se sequências aleatórias de tamanho fixo,
geradas pelo Sequence Manipulation Suite: (​http://www.bioinformatics.org/sms2/random_dna.html​).
O tempo foi obtido pela função GNU ​date, ​que para nossos testes, apresentou uma precisão
​ s comparações giraram em torno da escolha do algoritmo e
mais elevada, em relação ao GNU time. A
utilização do grep ou agrep, dependendo da circunstância.
Para uma melhor eficiência das comparações, utilizou-se a flag “-c” em todos os casos de
teste. Assim retorna apenas o número de ocorrências do padrão no texto.
Os testes foram feitos em uma máquina com sistema operacional Ubuntu 16.04, Processador
Intel® Core™ i7-3770 CPU @ 3.40GHz × 8 , Memória 7,7 GiB, Gráficos Intel® Ivybridge Desktop.
Compilador utilizado GCC/G++ 5.4.0.

3.2) Resultados
3.3) Conclusões
Como mostrado nas figuras, é possível perceber que o Boyer-Moore decresce seu tempo em
relação ao tamanho do padrão. O que indica sua eficiência em padrões de tamanho elevado. Dentre os
casamentos exatos de padrão único, o único algoritmo que destoa um pouco, em relação aos demais,
é o Shift-Or, vide a explicação da seção 3.1. Além disso, nosso Shift-Or torna-se impossível quando o
tamanho do padrão é superior a 100.
Para múltiplos padrões, o Aho-Corasick se manteve estável até a quantidade 100 de padrões
num arquivo. O Aho-Corasick teve um comportamento linear e perdeu para o Grep.
Já para o casamento aproximado, como previsto, o sellers apresenta um comportamento
linear. Ideal para um padrão superior a 100. Quando não, o Wu-Manber se mostra bastante superior,
em relação ao tempo.
Assim como nossas implementações, o análise em relação as ferramentas grep e Agrep, foi
bastante válido, visto que estas ferramentas são implementadas em C e possuem diversas otimizações
para a leitura de arquivo. Fica como um trabalho futuro otimizar nosso pmt para se aproximar ainda
mais das duas ferramentas citadas.
Vale notar que a diferença do Grep para o Wu-Manber é uma constante. Isso significa que o
uso de bitset ainda foi uma escolha infeliz que nos trouxe essa constante. O Shift-Or teve um péssimo
comportamento por um deslize, mas é tarde demais para mudar. Apesar de correto, o problema vem
porque em cada iteração estamos recalculando a heurística. Os resultados seriam melhores se, por
exemplo, repetíssemos o que fizemos com o Wu-Manber na chamada.

Você também pode gostar