Rendimiento de la variable ThreadLocal

86

¿Cuánto se lee de la ThreadLocalvariable más lento que del campo normal?

Más concretamente, ¿la creación de objetos simples es más rápida o más lenta que el acceso a ThreadLocalvariables?

Supongo que es lo suficientemente rápido para que tener una ThreadLocal<MessageDigest>instancia sea mucho más rápido que crear una instancia de MessageDigestcada vez. Pero, ¿eso también se aplica al byte [10] o al byte [1000], por ejemplo?

Editar: La pregunta es ¿qué sucede realmente cuando se ThreadLocalreciben llamadas ? Si ese es solo un campo, como cualquier otro, entonces la respuesta sería "siempre es más rápido", ¿verdad?

Sarmun
fuente
2
Un hilo local es básicamente un campo que contiene un mapa hash y una búsqueda donde la clave es el objeto del hilo actual. Por tanto, es mucho más lento pero rápido. :)
eckes
1
@eckes: ciertamente se comporta así, pero generalmente no se implementa de esta manera. En su lugar, los Threads contienen un mapa hash (no sincronizado) donde la clave es el ThreadLocalobjeto actual
sbk

Respuestas:

40

Ejecutar evaluaciones comparativas no publicadas ThreadLocal.gettoma alrededor de 35 ciclos por iteración en mi máquina. No mucho. En la implementación de Sun, un mapa hash de sondeo lineal personalizado en Threadmapas ThreadLocalde valores. Debido a que solo se accede a él mediante un único hilo, puede ser muy rápido.

La asignación de objetos pequeños requiere un número similar de ciclos, aunque debido al agotamiento de la caché, es posible que obtenga cifras algo más bajas en un bucle cerrado.

MessageDigestEs probable que la construcción sea ​​relativamente cara. Tiene bastante estado y la construcción pasa por el Providermecanismo SPI. Es posible que pueda optimizar, por ejemplo, clonando o proporcionando el Provider.

El hecho de que sea más rápido almacenar en caché en un ThreadLocallugar que crear no significa necesariamente que el rendimiento del sistema aumentará. Tendrá gastos generales adicionales relacionados con GC, lo que ralentiza todo.

A menos que su aplicación utilice mucho, MessageDigestes posible que desee considerar el uso de una caché convencional segura para subprocesos.

Tom Hawtin - tackline
fuente
5
En mi humilde opinión, la forma más rápida es simplemente ignorar el SPI y usar algo como new org.bouncycastle.crypto.digests.SHA1Digest(). Estoy bastante seguro de que ningún caché puede superarlo.
maaartinus
57

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 usar un acceso de campo regular, por supuesto), además de asegurar que el objeto ThreadLocal se arreglara cuando el Thread murió. Al actualizar esta respuesta en 2016, parece que la mayoría (¿todas?) De las JVM más nuevas usan un ThreadLocalMap con sondeo lineal. No estoy seguro del rendimiento de esos, pero no puedo imaginar que sea significativamente peor que la implementación anterior.

Por supuesto, el nuevo Object () también es muy rápido en estos días, y los recolectores de basura también son muy buenos para recuperar objetos de corta duración.

A menos que esté seguro de que la creación de objetos será costosa, o si necesita conservar algún estado hilo por hilo, es mejor optar por la solución de asignación más simple cuando sea necesario, y solo cambiar a una implementación ThreadLocal cuando un profiler le dice que lo necesita.

Bill Michell
fuente
4
+1 por ser la única respuesta para abordar realmente la pregunta.
cletus
¿Puede darme un ejemplo de una JVM moderna que no usa sondeo lineal para ThreadLocalMap? Java 8 OpenJDK todavía parece estar usando ThreadLocalMap con sondeo lineal. grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/…
Karthick
1
@Karthick Lo siento, no, no puedo. Escribí esto en 2009. Lo actualizaré.
Bill Michell
34

Buena pregunta, me lo he estado preguntando recientemente. Para darle números definidos, los puntos de referencia a continuación (en Scala, compilados virtualmente 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)
}

disponibles aquí , se realizaron en un AMD 4x 2.8 GHz de doble núcleo y un i7 de cuatro núcleos con hyperthreading (2.67 GHz).

Estos son los números:

i7

Especificaciones: Intel i7 2x quad-core @ 2.67 GHz Prueba: scala.threads.ParallelTests

Nombre de la prueba: loop_heap_read

Número de hilo: 1 Pruebas totales: 200

Tiempos de ejecución: (mostrando los últimos 5) 9.0069 9.0036 9.0017 9.0084 9.0074 (avg = 9.1034 min = 8.9986 max = 21.0306)

Número de hilo: 2 Pruebas totales: 200

Tiempos de ejecución: (mostrando los últimos 5) 4.5563 4.7128 4.5663 4.5617 4.5724 (avg = 4.6337 min = 4.5509 max = 13.9476)

Número de hilo: 4 Pruebas totales: 200

Tiempos de ejecución: (mostrando los últimos 5) 2,3946 2,3979 2,3934 2,3937 2,3964 (promedio = 2,5113 mínimo = 2,3884 máximo = 13,5496)

Número de hilo: 8 Pruebas totales: 200

Tiempos de ejecución: (mostrando los últimos 5) 2.4479 2.4362 2.4323 2.4472 2.4383 (avg = 2.5562 min = 2.4166 max = 10.3726)

Nombre de la prueba: threadlocal

Número de hilo: 1 Pruebas totales: 200

Tiempos de ejecución: (mostrando los últimos 5) 91.1741 90.8978 90.6181 90.6200 90.6113 (avg = 91.0291 min = 90.6000 max = 129.7501)

Número de hilo: 2 Pruebas totales: 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 hilo: 4 Pruebas totales: 200

Tiempos de ejecución: (mostrando los últimos 5) 22.8118 22.8135 59.1753 22.8229 22.8172 (prom = 23.9752 min = 22.7951 max = 59.1753)

Número de hilo: 8 Pruebas totales: 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 Prueba: scala.threads.ParallelTests

Nombre de la prueba: loop_heap_read

Trabajo total: 20000000 Número de hilo: 1 Pruebas totales: 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 max = 26,698)

Nombre de la 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 mínimo = 6.393 máximo = 19.716)

Número de hilo: 4 Pruebas totales: 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 max = 21.603)

Número de hilo: 8 Pruebas totales: 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 max = 19.755)

Nombre de la prueba: threadlocal

Número de hilo: 1 Pruebas totales: 200

Tiempos de ejecución: (mostrando los últimos 5) 200,217 207,335 200,241 207,342 200,23 (promedio = 202,2424 mínimo = 200,184 máximo = 245,369)

Número de hilo: 2 Pruebas totales: 200

Tiempos de ejecución: (mostrando los últimos 5) 100.208 100.199 100.211 103.781 100.215 (avg = 102.2238 min = 100.192 max = 129.505)

Número de hilo: 4 Pruebas totales: 200

Tiempos de ejecución: (mostrando los últimos 5) 62.101 67.629 62.087 52.021 55.766 (avg = 65.6361 min = 50.282 max = 167.433)

Número de hilo: 8 Pruebas totales: 200

Tiempos de ejecución: (mostrando los últimos 5) 40.672 74.301 34.434 41.549 28.119 (prom = 54.7701 min = 28.119 max = 94.424)

Resumen

Un subproceso local es aproximadamente 10-20 veces mayor que el del montón leído. También parece escalar bien en esta implementación de JVM y estas arquitecturas con la cantidad de procesadores.

axel22
fuente
5
+1 Felicitaciones por ser el único en dar resultados cuantitativos. Soy un poco escéptico porque estas pruebas están en Scala, pero como dijiste, los códigos de bytes de Java deberían ser similares ...
Gravity
¡Gracias! Este ciclo while da como resultado prácticamente el mismo código de bytes que produciría el código Java correspondiente. Sin embargo, se pueden observar diferentes tiempos en diferentes VM; esto se ha probado en un Sun JVM1.6.
axel22
Este código de referencia no simula un buen caso de uso para ThreadLocal. En el primer método: cada hilo tendrá una representación compartida en la memoria, la cadena no cambia. En el segundo método, se compara el costo de una búsqueda de tabla hash donde la cadena es disyuntiva entre todos los hilos.
Joelmob
La cadena no cambia, pero se lee de la memoria (la escritura de "!"nunca ocurre) en el primer método; el primer método es efectivamente equivalente a subclasificar Thready darle un campo personalizado. El punto de referencia mide un caso de borde extremo en el que todo el cálculo consiste en leer una variable / subproceso local; es posible que las aplicaciones reales no se vean afectadas según su patrón de acceso, pero en el peor de los casos, se comportarán como se indicó anteriormente.
axel22
4

Aquí va otra prueba. Los resultados muestran que ThreadLocal es un poco más lento que un campo normal, pero en el mismo orden. Aproximadamente un 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:

0-Muestra de campo en ejecución

Muestra de campo 0-End: 6044

0-Ejecución de muestra local de hilo

Muestra local de hilo 0-End: 6015

1-Muestra de campo en ejecución

Muestra de campo de 1 fin: 5095

1-Ejecución de muestra local de hilo

Muestra local de hilo de 1 extremo: 5720

Muestra de campo 2-Running

Muestra de campo de 2 extremos: 4842

Muestra local de 2 hilos en ejecución

Muestra local de hilo de 2 extremos: 5835

3-Muestra de campo en ejecución

Muestra de campo de 3 extremos: 4674

Muestra local de 3 hilos en ejecución

Muestra local de hilo de 3 extremos: 5287

Muestra de campo 4-Running

Muestra de campo de 4 extremos: 4849

Muestra local de 4 hilos en ejecución

Muestra local de hilo de 4 extremos: 5309

5-Muestra de campo en ejecución

Muestra de campo de 5 extremos: 4781

Muestra local de 5 hilos en ejecución

Muestra local de hilo de 5 extremos: 5330

6-Muestra de campo en ejecución

Muestra de campo de 6 extremos: 5294

Muestra local de 6 hilos en ejecución

Muestra local de hilo de 6 extremos: 5511

7-Ejecución de muestra de campo

Muestra de campo 7-End: 5119

Muestra local de 7 hilos en ejecución

Muestra local de hilo de 7 extremos: 5793

Muestra de campo de 8 ejecuciones

Muestra de campo de 8 extremos: 4977

Muestra local de 8 hilos en ejecución

Muestra local de hilo de 8 extremos: 6374

Muestra de campo 9-Running

Muestra de campo de 9 extremos: 4841

Muestra local de 9 hilos en ejecución

Muestra local de hilo de 9 extremos: 5471

Promedio de campo: 5051

Hilo Promedio local: 5664

Env:

versión de openjdk "1.8.0_131"

CPU Intel® Core ™ i7-7500U a 2,70 GHz × 4

Ubuntu 16.04 LTS

jpereira
fuente
Lo siento, esto ni siquiera se acerca a ser una prueba válida. A) Problema más grande: está asignando cadenas con cada iteración (lo Int.toString)cual es extremadamente costoso en comparación con lo que está probando. B) está haciendo dos operaciones de mapa en cada iteración, también totalmente sin relación y costosa. Intente incrementar un int primitivo de ThreadLocal en su lugar. C) Use en System.nanoTimelugar de System.currentTimeMillis, el primero es para crear perfiles, el segundo es para fines de fecha y hora del usuario y puede cambiar bajo sus pies. D) Debes evitar las asignaciones por completo, incluidas las de nivel superior para tus clases de "ejemplo"
Philip Guin
3

@Pete es la prueba correcta antes de optimizar.

Me sorprendería mucho si la construcción de un MessageDigest tiene una sobrecarga seria en comparación con su uso real.

Perder el uso de ThreadLocal puede ser una fuente de filtraciones y referencias colgantes, que no tienen un ciclo de vida claro, por lo general, nunca uso ThreadLocal sin un plan muy claro de cuándo se eliminará un recurso en particular.

Gareth Davis
fuente
0

Constrúyelo y mídelo.

Además, solo necesita un threadlocal si encapsula su comportamiento de digestión de mensajes en un objeto. Si necesita un MessageDigest local y un byte local [1000] para algún propósito, cree un objeto con un messageDigest y un campo byte [] y coloque ese objeto en ThreadLocal en lugar de ambos individualmente.

Pete Kirkham
fuente
Gracias, MessageDigest y byte [] son ​​usos diferentes, por lo que no se necesita un objeto.
Sarmun