java - Rendimiento de la variable ThreadLocal
multithreading performance (6)
@Pete es la prueba correcta antes de optimizar.
Me sorprendería mucho si la construcción de un MessageDigest tiene una sobrecarga importante en comparación con el uso actual.
La señorita que usa ThreadLocal puede ser una fuente de filtraciones y referencias colgantes, que no tienen un ciclo de vida claro, generalmente nunca uso ThreadLocal sin un plan muy claro de cuándo se eliminará un recurso en particular.
¿Cuánto se lee desde la variable ThreadLocal
más lento que desde el campo regular?
Más concretamente, ¿la creación de objetos simples es más rápida o más lenta que el acceso a la variable ThreadLocal
?
Supongo que es lo suficientemente rápido para que tener ThreadLocal<MessageDigest>
sea mucho más rápido que crear instancia de MessageDigest
cada vez. ¿Pero eso también se aplica para byte [10] o byte [1000] por ejemplo?
Editar: ¿Qué es lo que realmente sucede cuando se llama a ThreadLocal
? Si eso es solo un campo, como cualquier otro, entonces la respuesta sería "siempre es el más rápido", ¿verdad?
Aquí va otra prueba. Los resultados muestran que ThreadLocal es un poco más lento que un campo normal, pero en el mismo orden. Aprox 12% más lento
public class Test {
private static final int N = 100000000;
private static int fieldExecTime = 0;
private static int threadLocalExecTime = 0;
public static void main(String[] args) throws InterruptedException {
int execs = 10;
for (int i = 0; i < execs; i++) {
new FieldExample().run(i);
new ThreadLocaldExample().run(i);
}
System.out.println("Field avg:"+(fieldExecTime / execs));
System.out.println("ThreadLocal avg:"+(threadLocalExecTime / execs));
}
private static class FieldExample {
private Map<String,String> map = new HashMap<String, String>();
public void run(int z) {
System.out.println(z+"-Running field sample");
long start = System.currentTimeMillis();
for (int i = 0; i < N; i++){
String s = Integer.toString(i);
map.put(s,"a");
map.remove(s);
}
long end = System.currentTimeMillis();
long t = (end - start);
fieldExecTime += t;
System.out.println(z+"-End field sample:"+t);
}
}
private static class ThreadLocaldExample{
private ThreadLocal<Map<String,String>> myThreadLocal = new ThreadLocal<Map<String,String>>() {
@Override protected Map<String, String> initialValue() {
return new HashMap<String, String>();
}
};
public void run(int z) {
System.out.println(z+"-Running thread local sample");
long start = System.currentTimeMillis();
for (int i = 0; i < N; i++){
String s = Integer.toString(i);
myThreadLocal.get().put(s, "a");
myThreadLocal.get().remove(s);
}
long end = System.currentTimeMillis();
long t = (end - start);
threadLocalExecTime += t;
System.out.println(z+"-End thread local sample:"+t);
}
}
}''
Salida:
Muestra de campo de 0 carreras
Muestra del campo 0-End: 6044
Muestra local de 0-Running thread
Muestra local de 0-End thread: 6015
Muestra de campo de 1 ejecución
Muestra del campo 1-End: 5095
Muestra local de 1 ejecución de hilo
Muestra local del hilo de 1 extremo: 5720
Muestra de campo de 2 carreras
Muestra de campo de 2 extremos: 4842
Muestra local de 2-Running thread
Muestra local del hilo de 2 extremos: 5835
Ejemplo de campo de 3 carreras
Muestra de campo de 3 extremos: 4674
Muestra local de 3 secuencias de hilo
Muestra local de 3-End thread: 5287
Muestra de campo de 4 carreras
Muestra de campo de 4 extremos: 4849
Muestra local de 4-Running thread
Muestra local del hilo de 4 extremos: 5309
Ejemplo de campo de 5 carreras
Muestra de campo de 5 extremos: 4781
Muestra local de 5-Running thread
Muestra local del hilo de 5 extremos: 5330
Ejemplo de campo de 6 carreras
Muestra de campo de 6 extremos: 5294
Muestra local de 6-Running thread
Muestra local del hilo de 6 extremos: 5511
Muestra de campo de 7 carreras
Muestra de campo de 7 extremos: 5119
Muestra local de 7-Running thread
Muestra local del hilo de 7 extremos: 5793
Muestra de campo de 8 carreras
Muestra de campo de 8 extremos: 4977
Muestra local de 8-Running thread
Muestra local de 8 extremos de hilo: 6374
Muestra de campo de 9 carreras
Muestra de campo de 9 extremos: 4841
Muestra local de 9-Running thread
Muestra local del hilo de 9 extremos: 5471
Promedio de campo: 5051
ThreadLocal avg: 5664
Env:
versión de openjdk "1.8.0_131"
CPU Intel® Core ™ i7-7500U a 2.70GHz × 4
Ubuntu 16.04 LTS
Buena pregunta, me he estado preguntando eso recientemente. Para darle números definidos, los puntos de referencia a continuación (en Scala, compilados prácticamente con los mismos códigos de bytes que el código Java equivalente):
var cnt: String = ""
val tlocal = new java.lang.ThreadLocal[String] {
override def initialValue = ""
}
def loop_heap_write = {
var i = 0
val until = totalwork / threadnum
while (i < until) {
if (cnt ne "") cnt = "!"
i += 1
}
cnt
}
def threadlocal = {
var i = 0
val until = totalwork / threadnum
while (i < until) {
if (tlocal.get eq null) i = until + i + 1
i += 1
}
if (i > until) println("thread local value was null " + i)
}
disponible here , se realizaron en un AMD 4x a 2,8 GHz de doble núcleo y un quad-core i7 con hyperthreading (2,67 GHz).
Estos son los números:
i7
Especificaciones: Intel i7 2x quad-core a 2.67 GHz Prueba: scala.threads.ParallelTests
Nombre de la prueba: loop_heap_read
Número de tema: 1 Total de pruebas: 200
Tiempos de ejecución: (mostrando los últimos 5) 9.0069 9.0036 9.0017 9.0084 9.0074 (promedio = 9.1034 min = 8.9986 máximo = 21.0306)
Número de tema: 2 Total de pruebas: 200
Tiempos de ejecución: (mostrando los últimos 5) 4.5563 4.7128 4.5663 4.5617 4.5724 (promedio = 4.6337 min = 4.5509 max = 13.9476)
Número de tema: 4 Total de pruebas: 200
Tiempos de ejecución: (mostrando los últimos 5) 2.3946 2.3979 2.3934 2.3937 2.3964 (promedio = 2.5113 min = 2.3884 máximo = 13.5496)
Número de tema: 8 Total de pruebas: 200
Tiempos de ejecución: (mostrando los últimos 5) 2.4479 2.4362 2.4323 2.4472 2.4383 (promedio = 2.5562 min = 2.4166 max = 10.3726)
Nombre de prueba: threadlocal
Número de tema: 1 Total de pruebas: 200
Tiempos de ejecución: (mostrando los últimos 5) 91.1741 90.8978 90.6181 90.6200 90.6113 (promedio = 91.0291 min = 90.6000 max = 129.7501)
Número de tema: 2 Total de pruebas: 200
Tiempos de ejecución: (mostrando los últimos 5) 45.3838 45.3858 45.6676 45.3772 45.3839 (prom = 46.0555 min = 45.3726 max = 90.7108)
Número de tema: 4 Total de pruebas: 200
Tiempos de ejecución: (mostrando los últimos 5) 22.8118 22.8135 59.1753 22.8229 22.8172 (promedio = 23.9752 min = 22.7951 max = 59.1753)
Número de tema: 8 Total de pruebas: 200
Tiempos de ejecución: (mostrando los últimos 5) 22.2965 22.2415 22.3438 22.3109 22.4460 (prom = 23.2676 min = 22.2346 max = 50.3583)
AMD
Especificaciones: AMD 8220 4x dual-core @ 2.8 GHz Test: scala.threads.ParallelTests
Nombre de la prueba: loop_heap_read
Trabajo total: 20000000 Número de hilo: 1 Total de pruebas: 200
Tiempos de ejecución: (mostrando los últimos 5) 12.625 12.631 12.634 12.632 12.628 (promedio = 12.7333 min = 12.619 máximo = 26.698)
Nombre de prueba: loop_heap_read Trabajo total: 20000000
Tiempos de ejecución: (mostrando los últimos 5) 6.412 6.424 6.408 6.397 6.43 (promedio = 6.5367 min = 6.393 máximo = 19.716)
Número de tema: 4 Total de pruebas: 200
Tiempos de ejecución: (mostrando los últimos 5) 3.385 4.298 9.7 6.535 3.385 (promedio = 5.6079 min = 3.354 máximo = 21.603)
Número de tema: 8 Total de pruebas: 200
Tiempos de ejecución: (mostrando los últimos 5) 5.389 5.795 10.818 3.823 3.824 (promedio = 5.5810 min = 2.405 máximo = 19.755)
Nombre de prueba: threadlocal
Número de tema: 1 Total de pruebas: 200
Tiempos de ejecución: (mostrando los últimos 5) 200.217 207.335 200.241 207.342 200.23 (promedio = 202.2424 min = 200.184 máximo = 245.369)
Número de tema: 2 Total de pruebas: 200
Tiempos de ejecución: (mostrando los últimos 5) 100.208 100.199 100.211 103.781 100.215 (promedio = 102.2238 min = 100.192 máximo = 129.505)
Número de tema: 4 Total de pruebas: 200
Tiempos de ejecución: (mostrando los últimos 5) 62.101 67.629 62.087 52.021 55.766 (promedio = 65.6361 min = 50.282 máximo = 167.433)
Número de tema: 8 Total de pruebas: 200
Tiempos de ejecución: (mostrando los últimos 5) 40.672 74.301 34.434 41.549 28.119 (promedio = 54.7701 min = 28.119 máximo = 94.424)
Resumen
Un hilo local es alrededor de 10-20x que el de la lectura de montón. También parece escalar bien en esta implementación de JVM y estas arquitecturas con la cantidad de procesadores.
Constrúyelo y mídelo.
Además, solo necesita un threadlocal si encapsula su comportamiento de digestión de mensaje en un objeto. Si necesita un MessageDigest local y un byte local [1000] para algún propósito, cree un objeto con un campo messageDigest y un byte [] y coloque ese objeto en ThreadLocal en lugar de ambos individualmente.
Ejecutando puntos de referencia no publicados, ThreadLocal.get
toma alrededor de 35 ciclos por iteración en mi máquina. No es un buen negocio En la implementación de Sun, un mapa hash de sondeo lineal personalizado en Thread
mapea ThreadLocal
s a valores. Debido a que solo se accede por un solo hilo, puede ser muy rápido.
La asignación de objetos pequeños toma una cantidad similar de ciclos, aunque debido al agotamiento de la memoria caché puede obtener cifras algo más bajas en un ciclo cerrado.
La construcción de MessageDigest
es relativamente costosa. Tiene una buena cantidad de estado y la construcción pasa por el mecanismo SPI del Provider
. Puede optimizar, por ejemplo, clonando o proporcionando el Provider
.
Solo porque pueda ser más rápido almacenar en caché un ThreadLocal
lugar de crear, no necesariamente significa que el rendimiento del sistema aumentará. Tendrás gastos generales adicionales relacionados con GC, lo que ralentiza todo.
A menos que su aplicación utilice en gran medida MessageDigest
, le conviene utilizar un caché convencional seguro para subprocesos.
En 2009, algunas JVM implementaron ThreadLocal utilizando un HashMap no sincronizado en el objeto Thread.currentThread (). Esto lo hizo extremadamente rápido (aunque no tan rápido como el uso de un acceso de campo regular, por supuesto), además de garantizar que el objeto ThreadLocal se arregló cuando el hilo murió. Al actualizar esta respuesta en 2016, parece que la mayoría (¿todos?) De las JVM más nuevas usan un ThreadLocalMap con sondeos lineales. No estoy seguro del rendimiento de esos, pero no puedo imaginar que sea significativamente peor que la implementación anterior.
Por supuesto, el nuevo Objeto () también es muy rápido en estos días, y los Recolectores de Basura también son muy buenos en la recuperación de objetos efímeros.
A menos que esté seguro de que la creación de objetos va a ser costosa, o necesita persistir en algún estado hilo por hilo, es mejor optar por la asignación más simple cuando sea necesaria, y solo cambiar a una implementación de ThreadLocal cuando Profiler te dice que debes hacerlo.