Espero que esta pregunta no se considere demasiado básica para este foro, pero ya veremos. Me pregunto cómo refactorizar un código para un mejor rendimiento que se ejecuta muchas veces.
Digamos que estoy creando una lista de frecuencia de palabras, usando un Mapa (probablemente un HashMap), donde cada clave es una Cadena con la palabra que se está contando y el valor es un Entero que se incrementa cada vez que se encuentra un token de la palabra.
En Perl, incrementar dicho valor sería trivialmente fácil:
$map{$word}++;
Pero en Java, es mucho más complicado. Aquí la forma en que lo estoy haciendo actualmente:
int count = map.containsKey(word) ? map.get(word) : 0;
map.put(word, count + 1);
Lo que, por supuesto, se basa en la función de autoboxing en las versiones más recientes de Java. Me pregunto si puede sugerir una forma más eficiente de incrementar dicho valor. ¿Hay incluso buenas razones de rendimiento para evitar el marco de Colecciones y usar algo más en su lugar?
Actualización: he hecho una prueba de varias de las respuestas. Vea abajo.
fuente
Respuestas:
Algunos resultados de la prueba
He recibido muchas buenas respuestas a esta pregunta, gracias amigos, así que decidí realizar algunas pruebas y descubrir qué método es realmente el más rápido. Los cinco métodos que probé son estos:
Método
Esto es lo que hice ...
Resultados
Presentaré los resultados primero y el código a continuación para aquellos que estén interesados.
El método ContainsKey fue, como se esperaba, el más lento, por lo que daré la velocidad de cada método en comparación con la velocidad de ese método.
Conclusiones
Parece que solo el método MutableInt y el método Trove son significativamente más rápidos, ya que solo dan un aumento de rendimiento de más del 10%. Sin embargo, si el subproceso es un problema, AtomicLong podría ser más atractivo que los demás (no estoy realmente seguro). También ejecuté TestForNull con
final
variables, pero la diferencia fue insignificante.Tenga en cuenta que no he perfilado el uso de memoria en los diferentes escenarios. Me alegraría saber de cualquiera que tenga una buena idea de cómo los métodos MutableInt y Trove podrían afectar el uso de la memoria.
Personalmente, considero que el método MutableInt es el más atractivo, ya que no requiere cargar ninguna clase de terceros. Entonces, a menos que descubra problemas con él, esa es la forma en que es más probable que vaya.
El código
Aquí está el código crucial de cada método.
Contiene clave
TestForNull
AtomicLong
Trove
MutableInt
fuente
freq.compute(word, (key, count) -> count == null ? 1 : count + 1)
? Internamente, realiza una búsqueda menos hash quecontainsKey
, sería interesante ver cómo se compara con los demás, debido a la lambda.Ahora hay una forma más corta con Java 8 usando
Map::merge
.Que hace:
Más información aquí .
fuente
map.merge(key, 1, (a, b) -> a + b);
sí lo hizoInteger::sum
como BiFunction, y no le gustó @russter responder de la manera en que fue escrito. Esto funcionó para míMap.merge(key, 1, { a, b -> a + b})
Un poco de investigación en 2016: https://github.com/leventov/java-word-count , código fuente de referencia
Mejores resultados por método (más pequeño es mejor):
Resultados de tiempo \ espacio:
fuente
Google Guava es tu amigo ...
... al menos en algunos casos. Tienen este bonito AtomicLongMap . Especialmente agradable porque se trata de tiempo como valor en su mapa.
P.ej
También es posible agregar más de 1 al valor:
fuente
AtomicLongMap#getAndAdd
toma unalong
clase primitiva y no la envoltura; No tiene sentido hacerlonew Long()
. YAtomicLongMap
es un tipo parametrizado; deberías haberlo declarado comoAtomicLongMap<String>
.@Hank Gay
Como seguimiento de mi propio comentario (bastante inútil): Trove parece el camino a seguir. Si, por cualquier razón, que quería seguir con el JDK estándar, ConcurrentMap y AtomicLong puede hacer que el código de una pequeña agradable poco, aunque tu caso es distinto.
saldrá
1
como el valor en el mapa parafoo
. Siendo realistas, todo lo que este enfoque tiene para recomendar es una mayor amistad con los hilos.fuente
Y así es como incrementas un valor con un código simple.
Beneficio:
Abajo:
Teóricamente, una vez que llama a get (), ya sabe dónde poner (), por lo que no debería tener que buscar nuevamente. Pero la búsqueda en el mapa hash generalmente lleva un tiempo mínimo que puede ignorar este problema de rendimiento.
Pero si usted es muy serio sobre el tema, es un perfeccionista, otra forma es usar el método de combinación, esto es (probablemente) más eficiente que el fragmento de código anterior, ya que (teóricamente) buscará en el mapa solo una vez: (aunque Este código no es obvio a primera vista, es corto y eficaz)
Sugerencia: debe preocuparse por la legibilidad del código más que por un pequeño aumento de rendimiento en la mayoría de las veces. Si le resulta más fácil entender el primer fragmento de código, úselo. Pero si eres capaz de entender bien el segundo, ¡también puedes hacerlo!
fuente
Siempre es una buena idea mirar la Biblioteca de colecciones de Google para este tipo de cosas. En este caso, un Multiset hará el truco:
Existen métodos de tipo Mapa para iterar sobre las claves / entradas, etc. Internamente, la implementación actualmente utiliza a
HashMap<E, AtomicInteger>
, por lo que no incurrirá en costos de boxeo.fuente
count()
método en un conjunto múltiple se ejecuta en tiempo O (1) u O (n) (peor caso)? Los documentos no están claros sobre este punto.Debe tener en cuenta el hecho de que su intento original
contiene dos operaciones potencialmente caras en un mapa, a saber,
containsKey
yget
. El primero realiza una operación potencialmente bastante similar a la segunda, ¡así que estás haciendo el mismo trabajo dos veces !Si observa la API de Map, las
get
operaciones generalmente regresannull
cuando el mapa no contiene el elemento solicitado.Tenga en cuenta que esto hará una solución como
peligroso, ya que puede producir
NullPointerException
s. Deberías comprobar pornull
primera vez.También tenga en cuenta , y esto es muy importante, que
HashMap
s puede contenernulls
por definición. Entonces, no todos los devueltosnull
dicen "no existe tal elemento". En este sentido,containsKey
se comporta de forma diferente a partirget
de realidad que le dice si hay un elemento de este tipo. Consulte la API para más detalles.Para su caso, sin embargo, es posible que no desee distinguir entre un almacenado
null
y "noSuchElement". Si no desea permitirnull
s, puede preferir aHashtable
. El uso de una biblioteca de contenedor como ya se propuso en otras respuestas podría ser una mejor solución para el tratamiento manual, dependiendo de la complejidad de su aplicación.Para completar la respuesta (¡y olvidé ponerla al principio, gracias a la función de edición!), La mejor manera de hacerlo de forma nativa es
get
ingresar unafinal
variable, verificarnull
yput
volver a ingresarla con a1
. La variable debería serfinal
porque es inmutable de todos modos. Es posible que el compilador no necesite esta pista, pero es más claro de esa manera.Si no desea confiar en el autoboxing, debería decir algo como en su
map.put(new Integer(1 + i.getValue()));
lugar.fuente
Otra forma sería crear un número entero mutable:
Por supuesto, esto implica crear un objeto adicional, pero la sobrecarga en comparación con la creación de un número entero (incluso con Integer.valueOf) no debería ser tanto.
fuente
Puede utilizar el método computeIfAbsent en la
Map
interfaz proporcionada en Java 8 .¿El método
computeIfAbsent
verifica si la clave especificada ya está asociada con un valor o no? Si no hay ningún valor asociado, intenta calcular su valor utilizando la función de mapeo dada. En cualquier caso, devuelve el valor actual (existente o calculado) asociado con la clave especificada, o nulo si el valor calculado es nulo.En una nota al margen, si tiene una situación en la que varios subprocesos actualizan una suma común, puede echar un vistazo a la clase LongAdder. Bajo una alta contención, el rendimiento esperado de esta clase es significativamente mayor que
AtomicLong
, a expensas de un mayor consumo de espacio.fuente
La rotación de memoria puede ser un problema aquí, ya que cada boxeo de un int mayor o igual a 128 causa una asignación de objeto (ver Integer.valueOf (int)). Aunque el recolector de basura se ocupa de manera muy eficiente con objetos de corta duración, el rendimiento se verá afectado hasta cierto punto.
Si sabe que el número de incrementos realizados superará en gran medida el número de teclas (= palabras en este caso), considere usar un soporte int en su lugar. Phax ya presentó el código para esto. Aquí está de nuevo, con dos cambios (la clase de titular se hizo estática y el valor inicial se estableció en 1):
Si necesita un rendimiento extremo, busque una implementación de Mapa que se adapte directamente a los tipos de valores primitivos. jrudolph mencionó GNU Trove .
Por cierto, un buen término de búsqueda para este tema es "histograma".
fuente
En lugar de llamar a usesKey (), es más rápido llamar a map.get y verificar si el valor devuelto es nulo o no.
fuente
¿Estás seguro de que esto es un cuello de botella? ¿Has hecho algún análisis de rendimiento?
Intente usar el generador de perfiles de NetBeans (es gratuito y está integrado en NB 6.1) para ver los puntos de acceso.
Finalmente, una actualización de JVM (digamos de 1.5-> 1.6) es a menudo un refuerzo de rendimiento económico. Incluso una actualización en el número de compilación puede proporcionar buenos aumentos de rendimiento. Si está ejecutando en Windows y esta es una aplicación de clase de servidor, use -server en la línea de comandos para usar la JVM de Hotspot del servidor. En máquinas Linux y Solaris, esto se detecta automáticamente.
fuente
Hay un par de enfoques:
Use un aloritmo de bolsa como los conjuntos contenidos en Google Collections.
Cree un contenedor mutable que pueda usar en el Mapa:
Y use put ("word", new My ("Word")); Luego puede verificar si existe e incrementar al agregar.
Evite lanzar su propia solución usando listas, porque si obtiene una búsqueda y clasificación de bucles internos, su rendimiento apestará. La primera solución de HashMap es realmente bastante rápida, pero una propiedad como la que se encuentra en Google Collections es probablemente mejor.
Contar palabras usando Google Collections, se parece a esto:
Usar el HashMultiset es bastante elegante, porque un algoritmo de bolsa es justo lo que necesita al contar palabras.
fuente
Creo que su solución sería la forma estándar, pero, como usted mismo señaló, probablemente no sea la forma más rápida posible.
Puedes mirar GNU Trove . Esa es una biblioteca que contiene todo tipo de colecciones primitivas rápidas. Su ejemplo usaría un TObjectIntHashMap que tiene un método ajustarOrPutValue que hace exactamente lo que desea.
fuente
Una variación en el enfoque MutableInt que podría ser aún más rápido, si es un truco, es usar una matriz int de un solo elemento:
Sería interesante si pudiera volver a ejecutar sus pruebas de rendimiento con esta variación. Puede ser el más rápido.
Editar: El patrón anterior funcionó bien para mí, pero eventualmente cambié para usar las colecciones de Trove para reducir el tamaño de la memoria en algunos mapas muy grandes que estaba creando, y como beneficio adicional, también fue más rápido.
Una característica realmente agradable es que la
TObjectIntHashMap
clase tiene una solaadjustOrPutValue
llamada que, dependiendo de si ya hay un valor en esa clave, pondrá un valor inicial o incrementará el valor existente. Esto es perfecto para incrementar:fuente
HashMultiset de Google Collections:
bastante elegante de usar
, pero consume CPU y memoria
Lo mejor sería tener un método como:
Entry<K,V> getOrPut(K);
(elegante y de bajo costo)Tal método calculará el hash y el índice solo una vez, y luego podríamos hacer lo que queramos con la entrada (ya sea reemplazar o actualizar el valor).
Más elegante:
- tome un
HashSet<Entry>
- extiéndalo para que
get(K)
coloque una nueva entrada si es necesario- La entrada podría ser su propio objeto.
->
(new MyHashSet()).get(k).increment();
fuente
Muy simple, solo use la función incorporada de la
Map.java
siguiente manerafuente
++
... OMG, es muy simple. @siegi++
no funciona en ninguna parte de esta expresión porque se necesita una variable como su operando, pero solo hay valores. Su adición de+ 1
obras sin embargo. Ahora su solución es la misma que en la respuesta off99555 ."poner" necesita "obtener" (para garantizar que no haya una clave duplicada).
Entonces, haga un "put" directamente,
y si hubo un valor anterior, entonces agregue:
Si el recuento comienza en 0, agregue 1: (o cualquier otro valor ...)
Aviso: este código no es seguro para subprocesos. Úselo para construir, luego use el mapa, no para actualizarlo simultáneamente.
Optimización: en un bucle, mantenga el valor anterior para convertirse en el nuevo valor del siguiente bucle.
fuente
Los diversos envoltorios primitivos, por ejemplo,
Integer
son inmutables, por lo que no hay una forma más concisa de hacer lo que está pidiendo, a menos que pueda hacerlo con algo como AtomicLong . Puedo intentarlo en un minuto y actualizar. Por cierto, Hashtable es parte del marco de colecciones .fuente
Usaría Apache Collections Lazy Map (para inicializar valores a 0) y usaría MutableIntegers de Apache Lang como valores en ese mapa.
El mayor costo es tener que buscar el mapa dos veces en su método. En el mío tienes que hacerlo solo una vez. Simplemente obtenga el valor (se inicializará si está ausente) e increméntelo.
fuente
La estructura de datos de la biblioteca Java funcional
TreeMap
tiene unupdate
método en la última cabecera del tronco:Ejemplo de uso:
Este programa imprime "2".
fuente
@Vilmantas Baranauskas: Con respecto a esta respuesta, comentaría si tuviera los puntos de representación, pero no los tengo. Quería señalar que la clase Counter definida allí NO es segura para subprocesos ya que no es suficiente simplemente sincronizar inc () sin sincronizar el valor (). No se garantiza que otros hilos que llamen al valor () vean el valor a menos que se haya establecido una relación anterior con la actualización.
fuente
No sé qué tan eficiente es, pero el código a continuación también funciona. Debe definir un
BiFunction
al principio. Además, puede hacer más que solo incrementar con este método.la salida es
fuente
Si usa Eclipse Collections , puede usar a
HashBag
. Será el enfoque más eficiente en términos de uso de memoria y también funcionará bien en términos de velocidad de ejecución.HashBag
está respaldado por unMutableObjectIntMap
que almacena entradas primitivas en lugar deCounter
objetos. Esto reduce la sobrecarga de memoria y mejora la velocidad de ejecución.HashBag
proporciona la API que necesitarías ya que es unCollection
que también te permite consultar la cantidad de ocurrencias de un elemento.Aquí hay un ejemplo del Kata de Eclipse Collections .
Nota: Soy un committer para Eclipse Collections.
fuente
Sugiero usar Java 8 Map :: compute (). También considera el caso cuando una clave no existe.
fuente
mymap.merge(key, 1, Integer::sum)
?Dado que mucha gente busca en los temas de Java las respuestas de Groovy, así es como puede hacerlo en Groovy:
fuente
La manera simple y fácil en Java 8 es la siguiente:
fuente
Espero entender tu pregunta correctamente, estoy llegando a Java desde Python para poder empatizar con tu lucha.
si usted tiene
tu harías
¡Espero que esto ayude!
fuente