Performances de la variable ThreadLocal

86

Dans quelle mesure la lecture d'une ThreadLocalvariable est-elle plus lente que celle d'un champ normal?

Plus concrètement, la création d'objet simple est-elle plus rapide ou plus lente que l'accès à la ThreadLocalvariable?

Je suppose qu'il est assez rapide pour que l' ThreadLocal<MessageDigest>instance soit beaucoup plus rapide que la création d'une instance de MessageDigestchaque 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 ThreadLocal? Si ce n'est qu'un champ, comme n'importe quel autre, alors la réponse serait "c'est toujours le plus rapide", non?

Sarmun
la source
2
Un thread local est essentiellement un champ contenant un hashmap et une recherche où la clé est l'objet thread actuel. C'est donc beaucoup plus lent mais toujours rapide. :)
eckes
1
@eckes: il se comporte certainement comme ça, mais ce n'est généralement pas implémenté de cette façon. Au lieu de cela, Threads contient un hashmap (non synchronisé) où la clé est l' ThreadLocalobjet actuel
sbk

Réponses:

40

L'exécution de benchmarks non publiés ThreadLocal.getprend environ 35 cycles par itération sur ma machine. Pas grand-chose. Dans l'implémentation de Sun, une mappe de hachage de sonde linéaire personnalisée Threadmappe ThreadLocalles valeurs aux valeurs. Comme il n'est accessible que par un seul thread, il peut être très rapide.

L'allocation de petits objets prend un nombre similaire de cycles, 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 MessageDigestest susceptible d'être relativement coûteuse. Il a une bonne quantité d'état et la construction passe par le Providermécanisme SPI. Vous pourrez peut-être optimiser, par exemple, en clonant ou en fournissant le fichier Provider.

Ce n'est pas parce qu'il peut être plus rapide de mettre en cache dans un ThreadLocalplutôt que de créer que les performances du système augmenteront. Vous aurez des frais généraux supplémentaires liés au GC qui ralentissent tout.

À moins que votre application ne l'utilise très fortement, MessageDigestvous pouvez envisager d'utiliser à la place un cache thread-safe conventionnel.

Tom Hawtin - Tacle
la source
5
À mon humble avis, le moyen le plus rapide est simplement d'ignorer le SPI et d'utiliser quelque chose comme new org.bouncycastle.crypto.digests.SHA1Digest(). Je suis sûr qu'aucun cache ne peut le battre.
maaartinus
57

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

Bien sûr, le nouvel Object () est également très rapide ces jours-ci, et les Garbage Collectors sont également très bons pour récupérer des objets de courte durée.

Sauf si vous êtes certain que la création d'objet va coûter cher, ou si vous devez conserver un état sur une base thread par thread, vous feriez mieux d'opter pour la solution d'allocation plus simple en cas de besoin, et de ne basculer vers une implémentation ThreadLocal que lorsqu'un profiler vous indique que vous devez.

Bill Michell
la source
4
+1 pour être la seule réponse à réellement répondre à la question.
cletus
Pouvez-vous me donner un exemple de JVM moderne qui n'utilise pas de sondage linéaire pour ThreadLocalMap? Java 8 OpenJDK semble toujours utiliser ThreadLocalMap avec un sondage linéaire. grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/…
Karthick
1
@Karthick Désolé non je ne peux pas. Je l'ai écrit en 2009. Je vais mettre à jour.
Bill Michell
34

Bonne question, je me suis posé la question récemment. Pour vous donner des chiffres précis, les benchmarks ci-dessous (en Scala, compilés avec pratiquement les mêmes bytecodes que le code Java équivalent):

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).

Voici les chiffres:

i7

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

Nom du test: loop_heap_read

Numéro de la discussion: 1 Total des 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)

Numéro de la discussion: 2 Total des tests: 200

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

Numéro de la discussion: 4 Total des tests: 200

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

Numéro de la discussion: 8 Total des tests: 200

Temps d'exécution: (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

Numéro de la discussion: 1 Total des 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)

Numéro de la discussion: 2 Total des tests: 200

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

Numéro de la discussion: 4 Total des 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)

Numéro de la discussion: 8 Total des 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 N ° de la discussion: 1 Total des tests: 200

Temps de fonctionnement: (montrant les 5 derniers) 12,625 12,631 12,634 12,632 12,628 (moy = 12,7333 min = 12,619 max = 26,698)

Nom du test: loop_heap_read Travail total: 20000000

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

Numéro de la discussion: 4 Total des tests: 200

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

Numéro de la discussion: 8 Total des tests: 200

Temps d'exécution: (montrant les 5 derniers) 5,389 5,795 10,818 3,823 3,824 (moy = 5,5810 min = 2,405 max = 19,755)

Nom du test: threadlocal

Numéro de la discussion: 1 Total des tests: 200

Temps d'exécution: (montrant les 5 derniers) 200,217 207,335 200,241 207,342 200,23 (moy = 202,2424 min = 200,184 max = 245,369)

Numéro de la discussion: 2 Total des tests: 200

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

Numéro de la discussion: 4 Total des tests: 200

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

Numéro de la discussion: 8 Total des tests: 200

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

Sommaire

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

axel22
la source
5
+1 Félicitations pour être le seul à donner des résultats quantitatifs. Je suis un peu sceptique car ces tests sont en Scala, mais comme vous l'avez dit, les bytecodes Java devraient être similaires ...
Gravity
Merci! Cette boucle while donne pratiquement le même bytecode que le code Java correspondant produit. Cependant, des moments différents peuvent être observés sur différentes machines virtuelles - cela a été testé sur une JVM1.6 Sun.
axel22
Ce code de référence ne simule pas un bon cas d'utilisation de ThreadLocal. Dans la première méthode: chaque thread aura une représentation partagée en mémoire, la chaîne ne change pas. Dans la deuxième méthode, vous comparez le coût d'une recherche de table de hachage où la chaîne est disjonctive entre tous les threads.
Joelmob
La chaîne ne change pas, mais elle est lue à partir de la mémoire (l'écriture de "!"ne se produit jamais) dans la première méthode - la première méthode équivaut effectivement à sous Thread- classer et à lui donner un champ personnalisé. Le benchmark mesure un cas extrême où tout le calcul consiste à lire une variable / thread local - les applications réelles peuvent ne pas être affectées en fonction de leur modèle d'accès, mais dans le pire des cas, elles se comporteront comme ci-dessus.
axel22
4

Ici, il va un autre test. Les résultats montrent que ThreadLocal est un peu plus lent qu'un champ normal, mais dans le même ordre. Environ 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 terrain 0-End: 6044

Exemple local de thread 0-Running

Échantillon local de thread 0-End: 6015

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

Échantillon de terrain à 1 extrémité: 5095

1-Exécution d'un exemple local de thread

Exemple local de thread à 1 extrémité: 5720

Échantillon de terrain à 2 courses

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

Exemple de thread local à 2 exécutions

Exemple local de thread à 2 extrémités: 5835

Échantillon de terrain à 3 courses

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

Exemple local de thread à 3 exécutions

Exemple local de thread à 3 extrémités: 5287

Échantillon de terrain 4-Running

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

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

Exemple local de thread à 4 extrémités: 5309

Échantillon de terrain à 5 courses

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

Exemple local de thread à 5 exécutions

Exemple local de thread à 5 extrémités: 5330

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

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

6-Exécution d'un exemple local de thread

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

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

Échantillon de terrain à 7 extrémités: 5119

7-Exécution d'un exemple local de thread

Exemple local de thread à 7 extrémités: 5793

8-échantillon de terrain en cours d'exécution

Échantillon de terrain à 8 extrémités: 4977

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

Exemple local de thread à 8 extrémités: 6374

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

Échantillon de terrain à 9 extrémités: 4841

9-Exécution d'un exemple local de thread

Exemple local de thread à 9 extrémités: 5471

Terrain moyen: 5051

ThreadMoyenne locale: 5664

Env:

version openjdk "1.8.0_131"

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

Ubuntu 16.04 LTS

jpereira
la source
Désolé, ce n'est même pas près d'être un test valide. A) Le plus gros problème: vous allouez des chaînes à chaque itération ( Int.toString)ce qui est extrêmement cher par rapport à ce que vous testez. B) vous faites deux opérations de carte à chaque itération, également totalement indépendantes et coûteuses. Essayez plutôt d'incrémenter un int primitif à partir de ThreadLocal. C) Utilisez à la System.nanoTimeplace de System.currentTimeMillis, le premier est pour le profilage, le second est à des fins de date-heure de l'utilisateur et peut changer sous vos pieds. D) Vous devriez éviter complètement les allocs, y compris ceux de niveau supérieur pour vos classes "d'exemple"
Philip Guin
3

@Pete est un test correct avant d'optimiser.

Je serais très surpris si la construction d'un MessageDigest a une surcharge sérieuse par rapport à son utilisation active.

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

Gareth Davis
la source
0

Construisez-le et mesurez-le.

De plus, vous n'avez besoin que d'un threadlocal si vous encapsulez votre comportement de digestion de message dans un objet. Si vous avez besoin d'un MessageDigest local et d'un octet local [1000] dans un but précis, créez un objet avec un messageDigest et un champ byte [] et placez cet objet dans ThreadLocal plutôt que les deux individuellement.

Pete Kirkham
la source
Merci, MessageDigest et byte [] sont des utilisations différentes, donc un objet n'est pas nécessaire.
Sarmun