java - ¿Eliminar() es más rápido que get() en HashMap?
performance benchmarking (2)
Cuando llama a remove()
después de la primera iteración, no hay nada que eliminar y no tiene que copiar el resultado (o más bien la referencia al resultado) en cualquier lugar (simplemente devuelve null
). Pero al llamar a get()
debe copiar o almacenar la referencia devuelta en algún lugar (no se ha buscado la implementación de Blackhole
) que requiere asignación de memoria y por lo tanto es más costoso que simplemente devolver el null
que podría optimizarse mediante JIT después de algunas iteraciones.
He escrito un punto de referencia para get
y remove
HashMap
siguiente manera:
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class HashMapBenchmark {
@State(Scope.Benchmark)
public static class Mystate {
HashMap<String,String> hashmapVar = new HashMap<String,String>();
String key0 = "bye";
@Setup(Level.Iteration)
public void setup(){
hashmapVar.put(key0,"bubye");
}
}
@Benchmark
public void hashmapGet(Mystate state ,Blackhole bh) {
bh.consume(state.hashmapVar.get(state.key0));
}
@Benchmark
public void hashmapRemove(Mystate state ,Blackhole bh) {
bh.consume(state.hashmapVar.remove(state.key0));
}
}
Produce el resultado de la siguiente manera:
Benchmark Mode Samples Score Score error Units
c.b.HashMapBenchmark.hashmapGet avgt 60 6.348 0.320 ns/op
c.b.HashMapBenchmark.hashmapRemove avgt 60 5.180 0.074 ns/op
Según el resultado, remove()
es más rápido que get()
ligeramente. Incluso para eliminar un elemento, primero tiene que recuperar el elemento, ¿no es así?
Entonces, ¿cómo es que remove()
es más rápido? ¿O me estoy perdiendo algo?
Actualización Después de usar el último JMH (1.11.3) y aquí está el resultado:
Benchmark Mode Cnt Score Error Units
HashMapBenchmark.hashmapGet avgt 60 9.713 ± 0.277 ns/op
HashMapBenchmark.hashmapRemove avgt 60 7.677 ± 0.166 ns/op
El problema es que estos puntos de referencia miden cosas diferentes: get()
de un mapa poblado y remove()
de un mapa vacío (eventualmente). La comparación no tiene sentido, y puedes descartar el punto de referencia.
Debe garantizar que la operación se realiza contra el mismo HashMap
. Desafortunadamente, eso requiere usar @Setup(Invocation)
, que es malo por sí mismo (¡lea el Javadoc!) O HashMap
los costos de construcción de HashMap
en el benchmark mismo:
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class HashMapBenchmark {
@Benchmark
public String get() {
HashMap<String, String> hm = createMap();
return hm.get("bye");
}
@Benchmark
public String remove() {
HashMap<String, String> hm = createMap();
return hm.remove("bye");
}
// extra protection from optimization
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
private HashMap<String, String> createMap() {
HashMap<String, String> hm = new HashMap<>();
hm.put("bye", "bye");
return hm;
}
}
Puede ser extremadamente cuidadoso y pelar la creación del mapa en un método independiente no alineado: los compiladores actuales no se optimizan en las llamadas. En mi i7-4790K, 4.0 GHz, Linux x86_64, JDK 8u66:
Benchmark Mode Cnt Score Error Units
HashMapBenchmark.get avgt 15 24.343 ± 0.351 ns/op
HashMapBenchmark.remove avgt 15 24.611 ± 0.369 ns/op
Sin diferencia drástica De hecho, si observa el código generado con -prof perfasm
, arrojaría algunas diferencias cuantificables allí. O bien, puede caracterizar rápidamente ambas cargas de trabajo con -prof perfnorm
.
Tenga en cuenta que este caso no responde si un método u otro mejor en mapas reales. El argumento podría hacerse para ambos: get
no modifica el mapa, y por lo tanto no causa almacenamientos de memoria, remove
puede ayudar a los factores de carga para que la siguiente remove
sea más rápida, etc. Un solo punto de referencia y un párrafo de texto están muy, muy lejos de cualquier discusión fructífera.