EP06 - Eficiência

EP06 - Eficiência

por Eduardo Laurentino -
Número de respostas: 10

Gente, 

Eu fiz o CircularSuffixArray seguindo as sugestões do checklist, isto é, com uma classe auxiliar CircularSuffix que representa o sufixo circular apenas implicitamente, sem criar novos objetos String, e implementando um Comparable para poder usar um Array.sort() na ordenação depois. Acredito que isso esteja dentro da performance requerida pelo enunciado (espaço proporcional a n+R, e tempo a nlogn).

Pergunta: com essa performance, eu deveria conseguir rodar o dickens.txt em tempo próximo a saida de teste que está no enunciado (segue)?

% time java CircularSuffixArray < textfiles/dickens.txt 

real    0m0.111s
user    0m0.120s
sys 0m0.004s


Pq na verdade estou demorando bem mais que isso (real time perto de 1m30s), então ou (i) não estou seguindo a performance indicada pelo enunciado como acho que estou ou (ii) essa saída do exemplo de teste é resultado de uma implementação ótima com performance muito melhor que a que o enunciado exige. E como o desempenho de BurrowsWheeler.transform() depende do desempenho de CircularSuffixArray, todo o EP se compromete aqui.

Alguém mais está tendo dificuldade com eficiência? Alguém fez de uma maneira melhor que a sugerida pelo checklist e poderia compartilhar uma dica? 

Em resposta à Eduardo Laurentino

Re: EP06 - Eficiência

por Bruna Thalenberg -

Lauren, acho que não dá pra chegar na performance desejada com Arrays.sort(). Aqui dickens.txt levava uns 3 minutos. Cheguei em ~16 segundos codando o sort na mão baseada no quick 3-way do Sedgewick. Isso no BurrowsWheeler, já que não dá muito pra comparar o CircularSuffix sem saber o que ele faz na main.  Dá uma olhada em

https://algs4.cs.princeton.edu/51radix/Quick3string.java.html

Em resposta à Eduardo Laurentino

Re: EP06 - Eficiência

por Bruna Thalenberg -

Outra coisa: não sei como você codou seu comparable, mas eu tinha feito aqui um que era recursivo. Então você pode até ter o sort n log n, mas, na prática, quando for comparar, vai ter ainda a recursão do comparador. Com os casos pequenos, tipo "abracadabra!", isso não aparece muito porque vão ser poucos os casos em que você vai ter que comparar mais de um par de letras pra cada sufixo, mas quando você vai fazer isso pra um texto enorme, isso transforma o programa numa carroça!

Não sei se tem como fazer o comparador sem ser recursivo (ou iterativo, mas que toma o mesmo tempo), no sentido de que você não pode olhar só pra dois caracteres e pronto. Com isso, você não atinge a performance que ele pede. Eu usei o quick 3-way com radix porque já tava olhando a página de radix mesmo por conta da dica que eles dão no passo a passo de usar o algoritmo pra contar as frequências. Como o LSD que a gente viu com o Yoshi só servia pra strings de mesmo tamanho e o MSD a gente não viu, fui por eliminação.

Qualquer coisa a gente conversa amanhã!

Em resposta à Bruna Thalenberg

Re: EP06 - Eficiência

por Eduardo Laurentino -

Como o Arrays.sort() é worst-case nlogn eu entendi que isso me deixaria dentro da performance requerida, então pensei que não haveria problema em usá-lo, sobretudo porque o meu comparable não é recursivo. 

Mas conversei com o Andrew aqui agora e entendi que embora não seja recursivo, o comparable é no pior caso linear no tamanho da string n, o que faz com que a minha ordenação, junto ao nlogn do Array.sort(), realize comparações no pior caso n²logn comparações, e como aparentemente não há como fazer um comparable melhor que O( n ), é por isso que usar o Array.sort() se torna um problema. 

Faz sentido pra mim agora pensar na razão de ir atrás de uma solução na ordenação que passe por um algoritmo de ordenação dedicado a ser eficiente com strings, adaptando pro no nosso caso como você fez, Bru. Valeu pela dica, vou dar um olhada no Quick3string que você compartilhou. 

 



Em resposta à Eduardo Laurentino

Re: EP06 - Eficiência

por José Coelho de Pina -

Ois,

Muito legal essa discussão!

O que o Arrays.sort() tem a dizer a respeito? O que diz a API?

Por favor, vocês poderiam testar o CircularSuffixArray com os arquivos aesop.txt, aesop-2copies.txt e aesop-4copies.txt e colocar aqui quanto tempo gastou?

Em resposta à José Coelho de Pina

Re: EP06 - Eficiência

por Bruna Thalenberg -

Tempo do BurrowsWheeler (minha main do CircularSuffix imprime muita sujeira pros testes):

time java-algs4 BurrowsWheeler - < ~/Downloads/aesop.txt > aesop.txt.bwt

real 0m0.694s
user 0m0.408s
sys 0m0.024s

bthalenberg@bthalenberg-K46CA ~/Documents/Coelho/Zip $ time java-algs4 BurrowsWheeler - < dickens.txt | java-algs4 MoveToFront - | java-algs4 Huffman - > out.txt

real 0m18.336s
user 0m20.444s
sys 0m0.224s
bthalenberg@bthalenberg-K46CA ~/Documents/Coelho/Zip $ time java-algs4 Huffman + < out.txt | java-algs4 MoveToFront + | java-algs4 BurrowsWheeler + > dickens.orig

real 0m4.558s
user 0m6.804s
sys 0m0.208s
bthalenberg@bthalenberg-K46CA ~/Documents/Coelho/Zip $ diff dickens.orig dickens.txt
bthalenberg@bthalenberg-K46CA ~/Documents/Coelho/Zip $


Por algum motivo aesop-2copies.txt e aesop-4copies.txt produzem um erro:

time java-algs4 BurrowsWheeler - < ~/Downloads/aesop-2copies.txt > aesop-2copies.txt.bwt


Exception in thread "main" java.lang.StringIndexOutOfBoundsException: String index out of range: -2988
at java.lang.String.charAt(String.java:658)
at CircularSuffixArray.charAt(CircularSuffixArray.java:33)
at CircularSuffixArray.less(CircularSuffixArray.java:70)
at CircularSuffixArray.insertion(CircularSuffixArray.java:64)
at CircularSuffixArray.sort(CircularSuffixArray.java:39)

...

at BurrowsWheeler.transform(BurrowsWheeler.java:9)
at BurrowsWheeler.main(BurrowsWheeler.java:72)

vou ver se encontro a causa triste

Em resposta à José Coelho de Pina

Re: EP06 - Eficiência

por Eduardo Laurentino -

Coelho, como comentei acima, fui atrás de ver a API do Array.sort() com o Andrew e entendi que como o número de comparações feito por ele é proporcional a nlogn e o meu método de comparação é linear em n (o tamanho da string, que no caso é a quantidade de sufixos circulares também; seria possível, nesse caso, fazer um método de comparação de strings para o CircularSuffix melhor que isso?), acabo realizando uma quantidade de comparações proporcional a n²log na ordenação como um todo, e este seria o gargalo de eficiência em usar uma ordenação genérica não dedicada a ser eficiente com strings. Isso faz sentido? 

Vou tentar melhorar minha implementação nesse sentido, com a dica da Bruna, e posso compartilhar novos resultados aqui depois. Por enquanto, ainda com o Arrays.sort(), essas são as minhas saídas:

time java-algs4 CircularSuffixArray < burrows/aesop.txt

real 0m0.538s
user 0m1.229s
sys 0m0.025s

time java-algs4 CircularSuffixArray < burrows/aesop-2copies.txt

real 0m57.248s
user 0m58.034s
sys 0m0.186s

time java-algs4 CircularSuffixArray < burrows/aesop-4copies.txt

real 11m22.198s
user 11m22.601s
sys 0m0.548s

Em resposta à José Coelho de Pina

Re: EP06 - Eficiência

por Bruna Thalenberg -

Professor, o programa será testado para o aesop-2copies ou o -4copies? Consegui consertar o bug (era um overflow de int!), mas leva uma vida. 

No enunciado, eles falam:

Performance requirements.   On typical English text, your data type must use space proportional to n + R (or better); the constructor must take time proportional to n log n (or better) on typical English text; and the methods length() and index() must take constant time.


What is meant by “typical English text inputs”? Inputs such as Aesop’s Fables, Moby Dick, or your most recent essay. We do not mean inputs with very long repeated substrings (such as aesop-2copies.txt or an input will 1 million consecutive As) or random inputs.

Fui pedir ajuda pros maratonistas de plantão (alô Nathan) e ele falou que uma forma de resolver isso envolveria hashing, fogos de artifício, piruetas ou outra forma bem menos trivial de construir o suffix array (vide https://web.stanford.edu/class/cs97si/suffix-array.pdf), e aí não cumpriria o requisito de espaço.

Posso dormir em paz?

Em resposta à Bruna Thalenberg

Re: EP06 - Eficiência

por José Coelho de Pina -

Ois,

Excelente discussão!

fui atrás de ver a API do Array.sort() com o Andrew e entendi que como o número de comparações feito por ele é proporcional a nlogn e o meu método de comparação é linear em n [...] Isso faz sentido?

Tudo que vocês pensaram faz sentido.
Arrays.sort() se apoio no compareTo() do objeto.
Em comparação de strings o que pega é o número de vezes que examinamos um caractere (charAt()).
Cada vez Arrays.sort() compara strings examina os seus prefixos.
Assim, uma simples comparação de chaves com prefixos comuns longos acaba custando muiiitoo caro.
Esse custo pode ser proporcional a n, no caso aesop-2copies.txt chega a ser n/2.
O ponto é: algoritmos genéricos examinam um mesmo caractere (charAt()) várias vezes.

Como discutimos ontem, LSD compara cada caractere ums vez e o consumo de tempo é proporcional a nW, onde W é o comprimento das palavras. O Espaço extra é n + R.

No caso de ordenação de sufixos circulares o consumo de tempo é \mathtt{n^2}, enquanto o tamanho da entrada é n. Assim, o consumo de tempo de LSD é quadrático.

MSD tem um comportamento de pior caso (com prefixos comuns longos) nW mais um sobrepeso de espaço extra proporcional a n + D x R onde D é a profundidade da recursão. O loop interno de MSD é meio pesado: ordenação por contagem.

Entretanto, em geral, para textos típos, MSD deve ter um compartamento sublinear: nem todo caractere das strings é examinado.

Bruna, você tem razão, aesop-2copies.txt, aesop-4copies.txt ... não são textos típico. É sacanagem. Mas, apesar de ser sacanagem, chama a atençaõ para questões interessantes (e ajudou a corrigir um problema na sua implementação original).

Dos algoritmo que conversamos, para textos típicos, 3-way string (radix) quicksort funciona muito bem. Esse algoritmo é, digamos, da família MSD. Para o EP de vocês, uma implementação adaptada da classe Quick3string de SW, como a Bruna sugere, tem um desempenho excelente.

Por fim, conversamos rapidamente sobre a ideia central dos algoritmos da família prefix-doubling. O algoritmo que executamos ontem na aula é dessa família e seu consumo de tempo é \mathtt{n \log n}. Por isso ele é muito rápido, inclusive com textos como aesop-2copies.txt e aesop-4copies.txt.

Como o Nathan falou para a Bruna, há muuuiittoo material sobre ordenação de sufixos. Coloquei com as notas de aula uma cópia do artigo A Taxonomy of Suffix Array Construction Algorithms. Nesse artigo os autores classificam vários algoritmos para ordenação de sufixos. Em particular sobre prefix-doubling algorithms eles atribuem a ideia original a Karp, Miller e Rosenberg (1972). Eles dizem que há dois prefix-doubling algorithms principais:

  • Manber e Myers (1993) que usa prefix-doubling com uma política MSD e
  • Larsson e Sadakane (1999) que usa prefix-doubling com 3-way (radix) quicksort de Bentley e McIlroy (1993).

O programa que executei na aula é uma implementação do algoritmo de Larsson e Sadakane.

Pausa para os comerciais: Richard Karp é um nome que vocês verão muito quando estudarem complexidade computacional. Vocês verão um pouquinho de complexidade computacional em Análise de Algoritmos. Mas realmente, para entender melhor essas coisa, seria legal cursar MAC0414 Autômatos, Computabilidade e Complexidade. Essa disciplina será oferecida no próximo semestre, Bem, eu acho essas coisas legais, mas aqui tem o meu viés teórico. Fim do comercial.

Posso dormir em paz?

Sempre.
Tudo é uma questão de manter.
A mente quieta.
A espinha ereta.
E o coração tranquilo.
Leila Pinheiro

Em resposta à Eduardo Laurentino

Re: EP06 - Eficiência

por Guilherme Thomaz -

O meu CircularSuffixArray tambem demora por volta de 2 a 3 minutos no construtor, mas isso nem esta relacionado ao sort(Eu acho!).

 

CircularSuffixArray < dickens.txt

real 2m13.334s
user 3m38.428s
sys 0m0.560s

 

Segui a implementacao sugerida pelo checklist e criei um array de objetos Circular Suffix, que guardam a referencia (int) para o incio da "substring" e que tem um comparator,

para usar o Quick3way. O sort e rapido, mas a criacao do array com milhares de posicoes demora.

 

Ai fica a minha duvida, os sufixos, nesse caso sao caracteres, palavras... ?

 

Por exemplo, meu programa fuciona assim:

 

Este e um texto

Este e um texto

ste e um textoE

te e um textoES

 

etc

 

Ou ele deveria considerar uma separacao por palavras

 

Este e um texto

 e um texto Este

um texto Este e 

 

etc?

 

Nao sei se fez algum sentido....