java multithreading performance thread-local

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.