7 Oct
Quando estudamos estruturas de dados, aprendemos que um Hash, no caso médio (ou no caso de um hashing perfeito) é mais rápido (possui menor complexidade) que uma árvore balanceada, como a árvore Rubro-Negra. A teoria nos prova isso. E na prática?
Rodei o seguinte programa para comparar os dois:
package net.vidageek.benchmark;
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;
public class DumbHashTreeBenchmark {
private static final int INCREMENTO = 500000;
private static final int MAX_BEFORE_CRASH = 2000001;
public static void main(String[] args) {
for (int i = 0; i < MAX_BEFORE_CRASH; i += INCREMENTO) {
Set hash = new HashSet();
Set tree = new TreeSet();
System.out.println("Teste: n = " + i + " Tempo Hash: "
+ popula(hash, i) + " Tempo Árvore: " + popula(tree, i));
}
}
private static long popula(Set set, int n) {
long inicio, fim;
inicio = System.currentTimeMillis();
for (int i = 0; i < n; i++)
set.add(i);
fim = System.currentTimeMillis();
return fim - inicio;
}
}
O resultado disso parece bem estranho (o tempo está em milisegundos):
| Benchmark | ||
|---|---|---|
| Itens inseridos | HashSet | TreeSet |
| 0 | 0 | 0 |
| 5×105 | 808 | 598 |
| 10×105 | 1102 | 797 |
| 15×105 | 2087 | 2381 |
| 20×105 | 1676 | 5439 |
Porque que nos dois primeiros testes o TreeSet foi mais eficiente que o HashSet? Em teoria o HashSet deveria ser mais rápido, não é? Não.
Usar um Hash é muito eficiente sob certas condições:
Embora eu não tenha visto a implementação da função de hashing, tenho certeza de que ela é boa. Uma empresa do tamanho da Sun não arriscaria o nome com algo tão besta. Então o problema só pode ser a tabela de hashing.
Como não disse qual deveria ser o tamanho da tabela, ela foi criada com o valor padrão (16). Como eu tentei inserir uma quantidade muito superior ao tamanho da tabela, ela teve que ficar sendo realocada. Essa operação chama-se re-hashing e é possui custo muito alto, porque envolve alocação de uma nova tabela maior que a anterior e cálculo da função de hashing de todos os elementos dentro dela. Basicamente um novo HashSet é contruído a cada vez que a tabela fica com itens demais. O tempo que é perdido com essa operação da uma vantagem bem grande para o TreeSet que acaba tendo um desempenho melhor por falha do programador. Para mostrar isso fiz uma pequena modificação no main do programa anterior:
public static void main(String[] args) {
for (int i = 0; i < MAX_BEFORE_CRASH; i += INCREMENTO) {
Set hash = new HashSet(2*MAX_BEFORE_CRASH);
Set tree = new TreeSet();
System.out.println("Teste: n = " + i + " Tempo Hash: "
+ popula(hash, i) + " Tempo Árvore: " + popula(tree, i));
}
}
Vejam a diferença assustadora dos resultados:
| Benchmark | ||
|---|---|---|
| Items inseridos | HashSet | TreeSet |
| 0 | 0 | 0 |
| 5×105 | 744 | 972 |
| 10×105 | 536 | 1365 |
| 15×105 | 979 | 3660 |
| 20×105 | 1730 | 4077 |
Ainda daria pra fazer o HashSet se comportar de forma mais rápida, movendo a criação dele para fora do laço e esvaziando ele no fim de cada iteração.
Em uma máquina com menos disponibilidade de mémoria (a que eu usei para o teste possui 2GB) se um HashSet for usado como da primeira maneira, ele será muito pior que o TreeSet. Por isso é muito importante saber sob quais condições uma estrutura de dados é melhor do que outra.
Posts Relacionados:
Assine nosso RSS feed!
10 Responses for "Benchmark TreeSet x HashSet"
Dae meu! Po curti teu blog e também porque to começando em java to procurando conhecer pessoas da area para me engajar. uma pergunta que é hash? porque eu sei q ue eu uso isso no utorrent mas eu não nada disso ai não. outra coisa vou por vc como amigo la no meu blog valeu.!
Oi Jonas
Algumas pessoas implementaram as Collections do java.util de maneira a evitar o rehashing gigante em tabelas de hash ou a realocacao de arrays no caso de ArrayList e afins. O objetivo é ter um tempo gasto médio, em vez de picos de consumo de CPU (fazendo nossa analise ser amortizada).
Uma dessas apis é até usada pela NASA, pois serve aos propositos de algumas aplicacoes realtime, nao estressa o garbagte collector e voce tem melhores previsoes de consumo de tempo. Essa é a Javolution:
http://javolution.org/
A diferença brutal deve-se ao fato que o TreeSet mantém a árvore ordenada, enquanto o HashSet não garante que a ordem se manterá constante com o tempo.
Tudo isso é muito bem explicado no JavaDoc dos mesmos.
E respondendo a dúvida sobre o função hash implementada nas classes Hash do Java:
static int hash(Object x) {
int h = x.hashCode();
h = ~(h >> 14);
h = (h >> 10);
return h;
}
O WordPress comeu parte do que postei acima…
Aqui tem o hash que o GNU implementa no GCJ:
http://www.google.com/codesearch?hl=pt-BR&q= HashMap.java show:ukwwUY-mhAE:DRmxMbjifok:HoPTo2E50cc&sa=N&cd=7&ct=rc&cs_p=http://ftp2.at.freebsd.org/pub/FreeBSD/distfiles/gcc-2.97.tar.gz&cs_f=gcc/libjava/java/util/HashMap.java#first
mas fiquem avisados que a implementação da Sun é _muito_ diferente.
Olá Akaihen,
O Hash do utorrent é um pouco diferente do que eu falei aqui no post. Lá ele pode significar duas coisas: pode ser informação sobre onde estão (na web) os pedaços de arquivos que você está baixando ou ele é o tipo de função (função de hashing) que é aplicado em cada pedaço que você recebe pra saber se o pedaço está certo oui se alguns bits estão alterados.
Aqui no post, Hash é diminutivo de HashTable, que é uma estrutura de dados que possui acesso, gravação e remoção muito rápidos. Se estiver interessado de uma olhada nos links que estão no post. Mais pra frente vou escrever um pouco sobre estruturas de dados aqui.
Paulo,
Obrigado pela dica.Dei uma olhada e parece bem interessante. Vou ver se pego o fonte para dar uma olhada.
Bruno,
Entendi o que você disse sobre manter a ordem dos elementos (faz sentido afinal, se você garante algo a mais, deve existir algum custo a mais também) mas a grande diferença dessas estruturas de dados é a disposição dos dados. O Hash possui acesso quase direto ao elemento (um hashing perfeito possui) usando uma função que devolve o lugar onde o item deve estar. A árvore, ao contrario, determina uma disposição entre os elemento, de forma que o único jeito de você chegar em um elemento é passando pelos outros. Esse caminho extra que existe na árvore é que causa essa diferença de complexidade (tempo gasto).
Quanto à função de hashing, no código que você postou você usou a hashCode que está implementada em Object. É essa função quem está primariamente determinando o código de hashing do seu item e nela está o segredo da velocidade do Hash. Quando eu encontrar uma boa implementação dela eu posto aqui no blog.
Muito obrigado a todos pelas visitas.
Acabei de olhar na api do Java 5 que a função hashCode() de Object costuma devolver o endereço na memória do objeto como seu código de hashing. Não consigo afirmar que essa seja uma boa implementação (não acho bom estar tão associado à porção de memória onde o programa está rodando) mas também não encontro um argumento forte contra tal implementação.
O link na api é esse: http://java.sun.com/j2se/1.5.0/docs/api/java/lang/Object.html#hashCode()
Um fato bastante importante que não foi observado é que, assintoticamente, mesmo com as realocações da tabela, o HashSet foi melhor do que o TreeSet. Logo, a teoria confirma a prática, sim.
Acho que só até certo ponto luiz. Em termos práticos, se temos uma função que consome tempo proporcional a 1000000 e outra que é proporcional a n, a linear ainda vai funcionar muito melhor na prática (estou pensando em um caso em que você trabalha com poucos itens - menos de 1000, o que é comum).
Embora a teoria nos diga que a a constante seja melhor (O(1) < O(n)), na práticao que acontece é que para quase todos os casos a linear produz um resultado muito mais rápido.
Então em um ambiente com pouca memória (que eleve ainda mais o custo de uma realocação) a árvore vai ser mais performática que o hashset.
Achei o artigo muito bom. Porém gostaria de destacar dois pontos:
1-Não achei informações sobre Árvore Rubro Negra no link citado
2-A função Hash feita pela SUN pode ser boa, mas não necessariamente vai ser boa para os dados utilizados por você. A qualidade de uma função Hash depende dos dados armazenados, entao ninguem melhor do que o programador para saber se uma função é ótima para os seus tipos de dados.
Olá Carlos,
realmente o link estava errado (copiei o de complexidade…). Acabei de corrigí-lo.
Quanto a função de hashing, é necessário um bom conhecimento teórico para fazer uma boa (coisa que muitos programadores não tem). Por isso fiz o benchmark em cima da implementação da SUN (quase todo mundo vai usar a dela sem nem pensar duas vezes).
Obrigado pela visita!
Leave a reply