Espero que esta pergunta não seja considerada muito básica para este fórum, mas veremos. Eu estou querendo saber como refatorar algum código para obter um melhor desempenho que está sendo executado várias vezes.
Digamos que eu esteja criando uma lista de frequência de palavras, usando um Mapa (provavelmente um HashMap), em que cada chave é uma String com a palavra que está sendo contada e o valor é um Inteiro que é incrementado cada vez que um token da palavra é encontrado.
No Perl, incrementar esse valor seria trivialmente fácil:
$map{$word}++;
Mas em Java, é muito mais complicado. Aqui da maneira que estou fazendo atualmente:
int count = map.containsKey(word) ? map.get(word) : 0;
map.put(word, count + 1);
É claro que depende do recurso de caixa automática nas versões mais recentes do Java. Gostaria de saber se você pode sugerir uma maneira mais eficiente de aumentar esse valor. Existem ainda boas razões de desempenho para evitar a estrutura de coleções e usar outra coisa?
Atualização: Eu fiz um teste de várias respostas. Ver abaixo.
fonte
Respostas:
Alguns resultados do teste
Eu recebi muitas respostas boas para essa pergunta - obrigado pessoal -, então decidi executar alguns testes e descobrir qual método é realmente mais rápido. Os cinco métodos que testei são os seguintes:
Método
Aqui está o que eu fiz ...
Resultados
Vou apresentar os resultados primeiro e o código abaixo para quem estiver interessado.
O método ContainsKey foi, como esperado, o mais lento, portanto, darei a velocidade de cada método em comparação com a velocidade desse método.
Conclusões
Parece que apenas o método MutableInt e o método Trove são significativamente mais rápidos, pois apenas eles oferecem um aumento de desempenho superior a 10%. No entanto, se o threading for um problema, o AtomicLong pode ser mais atraente que os outros (não tenho muita certeza). Também executei o TestForNull com
final
variáveis, mas a diferença era insignificante.Observe que não analisei o uso de memória nos diferentes cenários. Eu ficaria feliz em ouvir de alguém que tenha boas idéias sobre como os métodos MutableInt e Trove provavelmente afetarão o uso da memória.
Pessoalmente, acho o método MutableInt o mais atraente, pois não requer o carregamento de nenhuma classe de terceiros. Então, a menos que eu descubra problemas, é assim que provavelmente vou.
O código
Aqui está o código crucial de cada método.
ContainsKey
TestForNull
AtomicLong
Trove
MutableInt
fonte
freq.compute(word, (key, count) -> count == null ? 1 : count + 1)
? Internamente, ele faz uma pesquisa com menos hash do quecontainsKey
, seria interessante ver como ele se compara aos outros, por causa da lambda.Agora há uma maneira mais curta com o Java 8 usando
Map::merge
.O que faz:
Mais informações aqui .
fonte
map.merge(key, 1, (a, b) -> a + b);
funcionou #Integer::sum
como BiFunction e não gostou da resposta do @russter da maneira como foi escrita. Isso funcionou para mimMap.merge(key, 1, { a, b -> a + b})
Uma pequena pesquisa em 2016: https://github.com/leventov/java-word-count , código fonte de referência
Melhores resultados por método (quanto menor, melhor):
Tempo \ espaço resultados:
fonte
Google Guava é seu amigo ...
... pelo menos em alguns casos. Eles têm esse ótimo AtomicLongMap . Especialmente interessante porque você está lidando com o valor contido no seu mapa.
Por exemplo
Também é possível adicionar mais de 1 ao valor:
fonte
AtomicLongMap#getAndAdd
pega umalong
classe primitiva e não a wrapper; não faz sentidonew Long()
. EAtomicLongMap
é um tipo parametrizado; você deveria ter declarado comoAtomicLongMap<String>
.@Hank Gay
Como acompanhamento do meu comentário (bastante inútil): Trove parece o caminho a percorrer. Se, por qualquer razão, que queria ficar com o JDK padrão, ConcurrentMap e AtomicLong pode tornar o código um minúsculo pouco mais agradável, embora YMMV.
deixará
1
como o valor no mapa parafoo
. Realisticamente, o aumento da cordialidade com o encadeamento é tudo o que essa abordagem precisa para recomendá-lo.fonte
E é assim que você incrementa um valor com código simples.
Benefício:
Desvantagem:
Teoricamente, depois de chamar get (), você já sabe onde colocar (), para que não precise procurar novamente. Mas pesquisar no mapa de hash geralmente leva um tempo muito mínimo para você ignorar esse problema de desempenho.
Mas se você é muito sério sobre o assunto, é um perfeccionista, outra maneira é usar o método de mesclagem, isso é (provavelmente) mais eficiente que o snippet de código anterior, pois você (teoricamente) pesquisará o mapa apenas uma vez: (embora esse código não é óbvio à primeira vista, é curto e tem bom desempenho)
Sugestão: você deve se preocupar com a legibilidade do código mais do que com pouco ganho de desempenho na maioria das vezes. Se o primeiro trecho de código for mais fácil para você entender, use-o. Mas se você é capaz de entender a segunda multa, também pode ir em frente!
fonte
É sempre uma boa ideia procurar na Biblioteca de coleções do Google esse tipo de coisa. Nesse caso, um Multiset fará o truque:
Existem métodos semelhantes a mapas para iterar sobre chaves / entradas, etc. Internamente, a implementação atualmente usa a
HashMap<E, AtomicInteger>
, para que você não incorra em custos de boxe.fonte
count()
método em um multiset é executado no tempo O (1) ou O (n) (pior caso)? Os documentos não são claros neste ponto.Você deve estar ciente do fato de que sua tentativa original
contém duas operações potencialmente caras em um mapa, a saber,
containsKey
eget
. O primeiro executa uma operação potencialmente muito semelhante ao último, então você está fazendo o mesmo trabalho duas vezes !Se você olhar a API para Map, as
get
operações geralmente retornamnull
quando o mapa não contém o elemento solicitado.Observe que isso criará uma solução como
perigoso, pois pode produzir
NullPointerException
s. Você deve verificar pelanull
primeira vez.Observe também , e isso é muito importante, que
HashMap
s pode conternulls
por definição. Portanto, nem todos os retornadosnull
dizem "não existe esse elemento". A esse respeito,containsKey
comporta-se de maneira diferente deget
realmente dizer se existe esse elemento. Consulte a API para obter detalhes.Para o seu caso, no entanto, talvez você não queira distinguir entre um armazenado
null
e "noSuchElement". Se você não deseja permitirnull
s, pode preferir aHashtable
. Usar uma biblioteca de wrapper como já foi proposto em outras respostas pode ser uma solução melhor para o tratamento manual, dependendo da complexidade do seu aplicativo.Para completar a resposta (e eu esqueci de inseri-la no início, graças à função de edição!), A melhor maneira de fazer isso de forma nativa é
get
entrar em umafinal
variável, verificarnull
eput
retornar com a1
. A variável deve serfinal
porque é imutável de qualquer maneira. O compilador pode não precisar dessa dica, mas é mais claro assim.Se você não quiser confiar no autoboxing, deve dizer algo parecido
map.put(new Integer(1 + i.getValue()));
.fonte
Outra maneira seria criar um número inteiro mutável:
é claro que isso implica criar um objeto adicional, mas a sobrecarga em comparação à criação de um número inteiro (mesmo com o número inteiro.valueOf) não deve ser muito.
fonte
Você pode usar o método computeIfAbsent na
Map
interface fornecida no Java 8 .O método
computeIfAbsent
verifica se a chave especificada já está associada a um valor ou não? Se nenhum valor associado, ele tenta calcular seu valor usando a função de mapeamento fornecida. Em qualquer caso, ele retorna o valor atual (existente ou calculado) associado à chave especificada ou nulo se o valor calculado for nulo.Além disso, se você tiver uma situação em que vários threads atualizem uma soma comum, poderá dar uma olhada na classe LongAdder . Sob alta contenção, o rendimento esperado dessa classe é significativamente maior do que
AtomicLong
, às custas de um maior consumo de espaço.fonte
A rotação da memória pode ser um problema aqui, pois todo boxe de um int maior ou igual a 128 causa uma alocação de objeto (consulte Integer.valueOf (int)). Embora o coletor de lixo lide de maneira muito eficiente com objetos de vida curta, o desempenho sofrerá até certo ponto.
Se você souber que o número de incrementos feitos superará em grande parte o número de chaves (= palavras neste caso), considere usar um titular int. Phax já apresentou o código para isso. Aqui está novamente, com duas alterações (a classe de detentor estática e o valor inicial definido como 1):
Se você precisar de desempenho extremo, procure uma implementação de Mapa diretamente adaptada aos tipos de valor primitivo. jrudolph mencionou o GNU Trove .
A propósito, um bom termo de pesquisa para esse assunto é "histograma".
fonte
Em vez de chamar containsKey (), é mais rápido chamar map.get e verificar se o valor retornado é nulo ou não.
fonte
Tem certeza de que isso é um gargalo? Você já fez alguma análise de desempenho?
Tente usar o profiler do NetBeans (gratuito e embutido no NB 6.1) para verificar pontos de acesso.
Por fim, uma atualização da JVM (por exemplo, de 1.5 a 1.6) geralmente é um impulsionador de desempenho barato. Mesmo uma atualização no número da compilação pode fornecer um bom desempenho. Se você estiver executando no Windows e este for um aplicativo de classe de servidor, use -server na linha de comandos para usar a JVM do Hotspot do Servidor. Nas máquinas Linux e Solaris, isso é detectado automaticamente.
fonte
Existem algumas abordagens:
Use um aloritmo de bolsa como os conjuntos contidos nas coleções do Google.
Crie um contêiner mutável que você possa usar no mapa:
E use put ("word", novo My ("Word")); Depois, você pode verificar se existe e incrementar ao adicionar.
Evite lançar sua própria solução usando listas, porque se você buscar e classificar no interior do loop, seu desempenho será ruim. A primeira solução HashMap é realmente bastante rápida, mas provavelmente a mesma encontrada nas coleções do Google é melhor.
Contando palavras usando o Google Collections, é algo como isto:
O uso do HashMultiset é bastante elegante, porque um algoritmo de bolsa é exatamente o que você precisa ao contar palavras.
fonte
Acho que sua solução seria a maneira padrão, mas - como você se notou - provavelmente não é a maneira mais rápida possível.
Você pode olhar para o GNU Trove . Essa é uma biblioteca que contém todos os tipos de coleções primitivas rápidas. Seu exemplo usaria um TObjectIntHashMap que possui um método AdjustOrPutValue que faz exatamente o que você deseja.
fonte
Uma variação na abordagem MutableInt que pode ser ainda mais rápida, se for um hack, é usar uma matriz int de elemento único:
Seria interessante se você pudesse executar novamente seus testes de desempenho com essa variação. Pode ser o mais rápido.
Edit: O padrão acima funcionou bem para mim, mas eventualmente mudei para usar as coleções do Trove para reduzir o tamanho da memória em alguns mapas muito grandes que eu estava criando - e como bônus também foi mais rápido.
Um recurso realmente interessante é que a
TObjectIntHashMap
classe tem uma únicaadjustOrPutValue
chamada que, dependendo se já existe um valor nessa chave, colocará um valor inicial ou aumentará o valor existente. Isso é perfeito para incrementar:fonte
Google Collections HashMultiset:
- bastante elegante de usar
- mas consome CPU e memória
O melhor seria ter um método como:
Entry<K,V> getOrPut(K);
(elegante e de baixo custo)Esse método calculará o hash e o índice apenas uma vez e, em seguida, poderemos fazer o que queremos com a entrada (substituir ou atualizar o valor).
Mais elegante:
- faça um
HashSet<Entry>
- estenda-o para
get(K)
colocar uma nova entrada, se necessário- A entrada pode ser seu próprio objeto.
->
(new MyHashSet()).get(k).increment();
fonte
Muito simples, basta usar a função
Map.java
interna da seguinte maneirafonte
++
... OMG, é tão simples. @siegi++
não funciona em nenhum lugar nesta expressão porque uma variável é necessária como seu operando, mas há apenas valores. Sua adição de+ 1
obras embora. Agora sua solução é a mesma da resposta off99555s ."put" need "get" (para garantir que nenhuma chave duplicada).
Então, faça um "put" diretamente
e , se houver um valor anterior, faça uma adição:
Se a contagem começar em 0, adicione 1: (ou qualquer outro valor ...)
Aviso: Este código não é seguro para threads. Use-o para construir e, em seguida, use o mapa, não para atualizá-lo simultaneamente.
Otimização: em um loop, mantenha o valor antigo para se tornar o novo valor do próximo loop.
fonte
Os vários invólucros primitivos, por exemplo,
Integer
são imutáveis, então não há realmente uma maneira mais concisa de fazer o que você está pedindo, a menos que você possa fazê-lo com algo como AtomicLong . Eu posso tentar isso em um minuto e atualizar. Aliás, o Hashtable faz parte do Framework de coleções .fonte
Eu usaria o Mapa Preguiçoso do Apache Collections (para inicializar os valores em 0) e usaria o MutableIntegers do Apache Lang como valores nesse mapa.
O maior custo é ter que anexar o mapa duas vezes no seu método. No meu você tem que fazer isso apenas uma vez. Apenas obtenha o valor (será inicializado se ausente) e aumente-o.
fonte
A estrutura de dados da biblioteca Java Funcional
TreeMap
possui umupdate
método no cabeçalho de tronco mais recente:Exemplo de uso:
Este programa imprime "2".
fonte
@Vilmantas Baranauskas: Em relação a esta resposta, gostaria de comentar se tivesse os pontos de representante, mas não tenho. Eu queria observar que a classe Counter definida não é segura para threads, pois não é suficiente apenas sincronizar inc () sem sincronizar value (). Outros encadeamentos que chamam value () não têm garantia de ver o valor, a menos que tenha sido estabelecido um relacionamento antes da instalação com a atualização.
fonte
Eu não sei o quão eficiente é, mas o código abaixo também funciona. Você precisa definir um
BiFunction
no início. Além disso, você pode fazer mais do que apenas incrementar com esse método.saída é
fonte
Se você estiver usando o Eclipse Collections , poderá usar a
HashBag
. Será a abordagem mais eficiente em termos de uso de memória e também terá bom desempenho em termos de velocidade de execução.HashBag
é apoiado por umMutableObjectIntMap
que armazena ints primitivas em vez deCounter
objetos. Isso reduz a sobrecarga de memória e melhora a velocidade de execução.HashBag
fornece a API que você precisa, pois é umaCollection
também permite consultar o número de ocorrências de um item.Aqui está um exemplo do Eclipse Collections Kata .
Nota: Sou um colaborador das Coleções Eclipse.
fonte
Sugiro usar o Java 8 Map :: compute (). Ele considera o caso quando uma chave também não existe.
fonte
mymap.merge(key, 1, Integer::sum)
?Como muitas pessoas pesquisam nos tópicos Java respostas do Groovy, veja como você pode fazê-lo no Groovy:
fonte
A maneira simples e fácil no java 8 é a seguinte:
fonte
Espero que eu esteja entendendo sua pergunta corretamente, estou chegando ao Java a partir do Python para poder simpatizar com sua luta.
se você tem
você faria
Espero que isto ajude!
fonte