web-dev-qa-db-fra.com

Performances de la variable ThreadLocal

Combien la lecture de la variable ThreadLocal est-elle plus lente que celle du champ normal?

Plus concrètement, la création d'objets simples est-elle plus rapide ou plus lente que l'accès à la variable ThreadLocal?

Je suppose qu'il est assez rapide pour que ThreadLocal<MessageDigest> instance est beaucoup plus rapide que la création d'instance de MessageDigest à chaque fois. Mais cela vaut-il également pour l'octet [10] ou l'octet [1000] par exemple?

Edit: La question est de savoir ce qui se passe réellement lors de l'appel de get de ThreadLocal? Si c'est juste un champ, comme n'importe quel autre, alors la réponse serait "c'est toujours le plus rapide", non?

82
Sarmun

Exécution de benchmarks non publiés, ThreadLocal.get prend environ 35 cycles par itération sur ma machine. Pas grand chose. Dans l'implémentation de Sun, une carte de hachage de sondage linéaire personnalisée dans Thread mappe ThreadLocals aux valeurs. Parce qu'il n'est accessible que par un seul thread, il peut être très rapide.

L'allocation de petits objets prend un nombre de cycles similaire, bien qu'en raison de l'épuisement du cache, vous puissiez obtenir des chiffres légèrement inférieurs dans une boucle serrée.

La construction de MessageDigest est susceptible d'être relativement coûteuse. Il a un bon état et la construction passe par le mécanisme Provider SPI. Vous pouvez être en mesure d'optimiser, par exemple, en clonant ou en fournissant le Provider.

Le fait qu'il soit plus rapide de mettre en cache dans un ThreadLocal plutôt que de créer ne signifie pas nécessairement que les performances du système augmenteront. Vous aurez des frais généraux supplémentaires liés au GC qui ralentit tout.

À moins que votre application n'utilise très fortement MessageDigest, vous pouvez envisager d'utiliser à la place un cache thread-safe classique.

39

En 2009, certaines machines virtuelles Java ont implémenté ThreadLocal à l'aide d'un HashMap non synchronisé dans l'objet Thread.currentThread (). Cela l'a rendu extrêmement rapide (bien que pas aussi rapide que l'utilisation d'un accès régulier au champ, bien sûr), tout en garantissant que l'objet ThreadLocal était rangé lorsque le thread était mort. En mettant à jour cette réponse en 2016, il semble que la plupart (toutes?) Des machines virtuelles Java plus récentes utilisent un ThreadLocalMap avec une sonde linéaire. Je ne suis pas sûr de la performance de ceux-ci - mais je ne peux pas imaginer que ce soit nettement pire que la mise en œuvre précédente.

Bien sûr, le nouvel Object () est également très rapide de nos jours, et les Garbage Collectors sont également très bons pour récupérer des objets éphémères.

À moins que vous ne soyez certain que la création d'objets va coûter cher, ou si vous devez conserver un état sur une base thread par thread, il vaut mieux opter pour la solution d'allocation plus simple en cas de besoin, et passer uniquement à une implémentation ThreadLocal lorsqu'un profiler vous dit que vous en avez besoin.

55
Bill Michell

Bonne question, je me le demande récemment. Pour vous donner des chiffres précis, les benchmarks ci-dessous (dans Scala, compilés pratiquement aux mêmes bytecodes que l'équivalent Java code):

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 ici , ont été réalisées sur un AMD 4x 2,8 GHz dual-core et un quad-core i7 avec hyperthreading (2,67 GHz).

Ce sont les chiffres:

i7

Spécifications: Intel i7 2x quad-core @ 2,67 GHz Test: scala.threads.ParallelTests

Nom du test: loop_heap_read

Nombre de fils: 1 Nombre total de tests: 200

Temps d'exécution: (montrant les 5 derniers) 9.0069 9.0036 9.0017 9.0084 9.0074 (moy = 9.1034 min = 8.9986 max = 21.0306)

Nombre de fils: 2 Nombre total de tests: 200

Temps d'exécution: (en montrant les 5 derniers) 4,5563 4,7128 4,5663 4,5617 4,5724 (moyenne = 4,6337 min = 4,5509 max = 13,9476)

Nombre de fils: 4 Nombre total de tests: 200

Temps d'exécution: (montrant les 5 derniers) 2,3946 2,3979 2,3934 2,3937 2,3964 (moyenne = 2,5113 min = 2,3884 max = 13,5496)

Nombre de fils: 8 Nombre total de tests: 200

Temps d'exécution: (en montrant les 5 derniers) 2.4479 2.4362 2.4323 2.4472 2.4383 (moy = 2.5562 min = 2.4166 max = 10.3726)

Nom du test: threadlocal

Nombre de fils: 1 Nombre total de tests: 200

Temps d'exécution: (montrant les 5 derniers) 91.1741 90.8978 90.6181 90.6200 90.6113 (moy = 91.0291 min = 90.6000 max = 129.7501)

Nombre de fils: 2 Nombre total de tests: 200

Temps d'exécution: (montrant les 5 derniers) 45,3838 45,3858 45,6676 45,3772 45,3839 (moyenne = 46,0555 min = 45,3726 max = 90,7108)

Nombre de fils: 4 Nombre total de tests: 200

Temps d'exécution: (montrant les 5 derniers) 22.8118 22.8135 59.1753 22.8229 22.8172 (moy = 23.9752 min = 22.7951 max = 59.1753)

Nombre de fils: 8 Nombre total de tests: 200

Temps d'exécution: (montrant les 5 derniers) 22.2965 22.2415 22.3438 22.3109 22.4460 (moy = 23.2676 min = 22.2346 max = 50.3583)

AMD

Spécifications: AMD 8220 4x dual-core @ 2,8 GHz Test: scala.threads.ParallelTests

Nom du test: loop_heap_read

Travail total: 20000000 Nombre de fils: 1 Nombre total de tests: 200

Temps d'exécution: (en montrant les 5 derniers) 12,625 12,631 12,634 12,632 12,628 (moyenne = 12,7333 min = 12,619 max = 26,698)

Nom du test: loop_heap_read Travail total: 20000000

Temps d'exécution: (en montrant les 5 derniers) 6,412 6,424 6,408 6,397 6,43 (moyenne = 6,5367 min = 6,393 max = 19,716)

Nombre de fils: 4 Nombre total de tests: 200

Temps d'exécution: (montrant les 5 derniers) 3,385 4,298 9,7 6,535 3,385 (moyenne = 5,6079 min = 3,354 max = 21,603)

Nombre de fils: 8 Nombre total de tests: 200

Temps de fonctionnement: (montrant les 5 derniers) 5,389 5,795 10,818 3,823 3,824 (moyenne = 5,5810 min = 2,405 max = 19,755)

Nom du test: threadlocal

Nombre de fils: 1 Nombre total de tests: 200

Temps d'exécution: (en montrant les 5 derniers) 200.217 207.335 200.241 207.342 200.23 (moy = 202.2424 min = 200.184 max = 245.369)

Nombre de fils: 2 Nombre total de tests: 200

Temps d'exécution: (en montrant les 5 derniers) 100,208 100,199 100,211 103,781 100,215 (moyenne = 102,2238 min = 100,192 max = 129,505)

Nombre de fils: 4 Nombre total de tests: 200

Temps d'exécution: (montrant les 5 derniers) 62,101 67,629 62,087 52,021 55,766 (moyenne = 65,6361 min = 50,282 max = 167,433)

Nombre de fils: 8 Nombre total de tests: 200

Temps d'exécution: (montrant les 5 derniers) 40,672 74,301 34,434 41,549 28,119 (moyenne = 54,7701 min = 28,119 max = 94,424)

Sommaire

Un thread local est environ 10 à 20 fois supérieur à celui du tas lu. Il semble également bien évoluer sur cette implémentation JVM et ces architectures avec le nombre de processeurs.

34
axel22

Ici, il passe un autre test. Les résultats montrent que ThreadLocal est un peu plus lent qu'un champ normal, mais dans le même ordre. Aprox 12% plus lent

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);
    }
}
}'

Production:

Échantillon de champ 0-Running

Échantillon de champ 0-End: 6044

Échantillon local de fil d'exécution 0

Échantillon local de thread 0-End: 6015

Échantillon de terrain 1-Running

Échantillon sur le terrain à une extrémité: 5095

Exemple local de thread 1-Running

Échantillon local de thread 1 extrémité: 5720

Échantillon de terrain en cours d'exécution

Échantillon de terrain à 2 extrémités: 4842

Exemple local de thread 2-Running

Échantillon local de filetage à 2 extrémités: 5835

Échantillon de terrain 3-Running

Échantillon de terrain à 3 extrémités: 4674

Exemple local de thread 3-Running

Échantillon local de thread à 3 extrémités: 5287

4-Échantillon de terrain en cours d'exécution

Échantillon de terrain à 4 extrémités: 4849

Exemple local de thread 4-Running

Échantillon local d'unité d'exécution à 4 extrémités: 5309

5-Échantillon de terrain en cours d'exécution

Échantillon de champ à 5 extrémités: 4781

Échantillon local de 5 threads en cours d'exécution

Échantillon local d'unité d'exécution à 5 extrémités: 5330

Échantillon de terrain 6-Running

Échantillon de champ à 6 extrémités: 5294

Exemple local d'unité d'exécution 6

Échantillon local de thread à 6 extrémités: 5511

7-Échantillon de terrain en cours d'exécution

Échantillon sur le terrain à 7 extrémités: 5119

Exemple local de 7 threads en cours d'exécution

Échantillon local de thread à 7 extrémités: 5793

Échantillon de terrain 8-Running

Échantillon sur le terrain à 8 extrémités: 4977

Exemple local de threads 8-Running

Échantillon local de thread à 8 extrémités: 6374

9-Échantillon de terrain en cours d'exécution

Échantillon sur le terrain à 9 extrémités: 4841

Échantillon local de 9 threads en cours d'exécution

Échantillon local de thread à 9 extrémités: 5471

Moyenne du champ: 5051

ThreadLocal moyenne: 5664

Env:

version openjdk "1.8.0_131"

Processeur Intel® Core ™ i7-7500U à 2,70 GHz × 4

Ubuntu 16.04 LTS

5
jpereira

@Pete est un test correct avant d'optimiser.

Je serais très surpris si la construction d'un MessageDigest a un sérieux surcoût par rapport à son utilisation réelle.

Manquer d'utiliser ThreadLocal peut être une source de fuites et de références pendantes, qui n'ont pas de cycle de vie clair, généralement je n'utilise jamais ThreadLocal sans un plan très clair de quand une ressource particulière sera supprimée.

3
Gareth Davis

Construisez-le et mesurez-le.

En outre, vous n'avez besoin que d'un thread local si vous encapsulez votre comportement de digestion des messages dans un objet. Si vous avez besoin d'un MessageDigest local et d'un octet local [1000] dans un certain but, créez un objet avec un messageDigest et un champ byte [] et placez cet objet dans le ThreadLocal plutôt que les deux individuellement.

0
Pete Kirkham