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?
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 ThreadLocal
s 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.
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.
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:
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)
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)
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.
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
@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.
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.