Prestazioni della variabile ThreadLocal
-
03-07-2019 - |
Domanda
Quanto viene letto dalla ThreadLocal
variabile più lentamente rispetto al campo normale?
Più concretamente la creazione di oggetti semplici è più veloce o più lenta dell'accesso alla ThreadLocal<MessageDigest>
variabile?
Suppongo che sia abbastanza veloce, quindi avere MessageDigest
istanza è molto più veloce della creazione dell'istanza di <=> ogni volta. Ma questo vale anche per byte [10] o byte [1000] per esempio?
Modifica: la domanda è cosa sta realmente succedendo quando si chiama <=> get? Se questo è solo un campo, come un altro, la risposta sarebbe & Quot; è sempre la più veloce & Quot ;, giusto?
Soluzione
Eseguendo benchmark non pubblicati, ThreadLocal.get
richiede circa 35 cicli per iterazione sulla mia macchina. Non molto. Nell'implementazione di Sun una mappa hash di analisi lineare personalizzata in Thread
mappe ThreadLocal
s ai valori. Poiché è accessibile solo da un singolo thread, può essere molto veloce.
L'allocazione di piccoli oggetti richiede un numero simile di cicli, anche se a causa dell'esaurimento della cache è possibile ottenere cifre leggermente inferiori in un ciclo stretto.
La costruzione di MessageDigest
è probabilmente relativamente costosa. Ha una discreta quantità di stato e la costruzione passa attraverso il meccanismo SPI. Potresti essere in grado di ottimizzare, ad esempio, clonando o fornendo il Provider
.
Solo perché potrebbe essere più veloce memorizzare nella cache in <=> piuttosto che creare non significa necessariamente che le prestazioni del sistema aumenteranno. Avrai costi generali aggiuntivi relativi a GC che rallenta tutto.
A meno che l'applicazione non utilizzi molto <=>, potresti prendere in considerazione l'uso di una cache thread-safe convenzionale.
Altri suggerimenti
Nel 2009, alcune JVM hanno implementato ThreadLocal usando una HashMap non sincronizzata nell'oggetto Thread.currentThread (). Ciò lo ha reso estremamente veloce (anche se non altrettanto veloce di un normale accesso al campo, ovviamente), oltre a garantire che l'oggetto ThreadLocal sia stato riordinato quando il Thread è morto. Aggiornando questa risposta nel 2016, sembra che la maggior parte (tutte?) Le nuove JVM utilizzino una ThreadLocalMap con sondaggio lineare. Non sono sicuro delle prestazioni di quei & # 8211; ma non riesco a immaginare che sia significativamente peggio della precedente implementazione.
Ovviamente, anche il nuovo Object () è molto veloce in questi giorni, e anche i Garbage Collector sono molto bravi a recuperare oggetti di breve durata.
A meno che non si sia certi che la creazione di oggetti sia costosa o che sia necessario mantenere un certo stato su una base thread per thread, è meglio andare per l'allocazione più semplice quando è necessaria la soluzione e passare solo a ThreadLocal implementazione quando un profiler ti dice che è necessario.
Bella domanda, me lo sono chiesto di recente. Per darti numeri definiti, i benchmark di seguito (in Scala, compilati praticamente con gli stessi bytecode dell'equivalente codice Java):
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)
}
disponibile qui , sono stati eseguiti su un dual core AMD 4x da 2,8 GHz e un i7 quad-core con hyperthreading (2,67 GHz).
Questi sono i numeri:
i7
Specifiche: Intel i7 2x quad-core a 2,67 GHz Test: scala.threads.ParallelTests
Nome test: loop_heap_read
Numero discussione: 1 Test totali: 200
Tempi di esecuzione: (mostrando gli ultimi 5) 9.0069 9.0036 9.0017 9.0084 9.0074 (media = 9.1034 min = 8.9986 max = 21.0306)
Numero discussione: 2 Test totali: 200
Tempi di esecuzione: (mostrando gli ultimi 5) 4.5563 4.7128 4.5663 4.5617 4.5724 (media = 4.6337 min = 4.5509 max = 13.9476)
Numero discussione: 4 Test totali: 200
Tempi di esecuzione: (mostrando gli ultimi 5) 2.3946 2.3979 2.3934 2.3937 2.3964 (media = 2.5113 min = 2.3884 max = 13.5496)
Numero discussione: 8 Test totali: 200
Tempi di esecuzione: (mostrando gli ultimi 5) 2.4479 2.4362 2.4323 2.4472 2.4383 (media = 2.5562 min = 2.4166 max = 10.3726)
Nome test: threadlocal
Numero discussione: 1 Test totali: 200
Tempi di esecuzione: (mostrando gli ultimi 5) 91.1741 90.8978 90.6181 90.6200 90.6113 (media = 91.0291 min = 90.6000 max = 129.7501)
Numero discussione: 2 Test totali: 200
Tempi di esecuzione: (mostrando gli ultimi 5) 45.3838 45.3858 45.6676 45.3772 45.3839 (media = 46.0555 min = 45.3726 max = 90.7108)
Numero discussione: 4 Test totali: 200
Tempi di esecuzione: (mostrando gli ultimi 5) 22.8118 22.8135 59.1753 22.8229 22.8172 (media = 23.9752 min = 22.7951 max = 59.1753)
Numero discussione: 8 Test totali: 200
Tempi di esecuzione: (mostrando gli ultimi 5) 22.2965 22.2415 22.3438 22.3109 22.4460 (media = 23.2676 min = 22.2346 max = 50.3583)
AMD
Specifiche: AMD 8220 4x dual-core a 2,8 GHz Test: scala.threads.ParallelTests
Nome test: loop_heap_read
Totale lavoro: 20000000 Numero discussione: 1 Test totali: 200
Tempi di esecuzione: (mostrando gli ultimi 5) 12.625 12.631 12.634 12.632 12.628 (media = 12.7333 min = 12.619 max = 26.698)
Nome test: loop_heap_read Totale lavoro: 20000000
Tempi di esecuzione: (mostrando gli ultimi 5) 6.412 6.424 6.408 6.397 6.43 (media = 6.5367 min = 6.393 max = 19.716)
Numero discussione: 4 Test totali: 200
Tempi di esecuzione: (mostrando gli ultimi 5) 3.385 4.298 9.7 6.535 3.385 (media = 5.6079 min = 3.354 max = 21.603)
Numero discussione: 8 Test totali: 200
Tempi di esecuzione: (mostrando gli ultimi 5) 5.389 5.795 10.818 3.823 3.824 (media = 5.5810 min = 2.405 max = 19.755)
Nome test: threadlocal
Numero discussione: 1 Test totali: 200
Tempi di esecuzione: (mostrando gli ultimi 5) 200.217 207.335 200.241 207.342 200.23 (media = 202.2424 min = 200.184 max = 245.369)
Numero discussione: 2 Test totali: 200
Tempi di esecuzione: (mostrando gli ultimi 5) 100.208 100.199 100.211 103.781 100.215 (media = 102.2238 min = 100.192 max = 129.505)
Numero discussione: 4 Test totali: 200
Tempi di esecuzione: (mostrando gli ultimi 5) 62.101 67.629 62.087 52.021 55.766(media = 65.6361 min = 50.282 max = 167.433)
Numero discussione: 8 Test totali: 200
Tempi di esecuzione: (mostrando gli ultimi 5) 40.672 74.301 34.434 41.549 28.119 (media = 54.7701 min = 28.119 max = 94.424)
Sommario
Un thread locale è circa 10-20x quello dell'heap letto. Sembra inoltre ridimensionare bene su questa implementazione JVM e queste architetture con il numero di processori.
Qui va un altro test. I risultati mostrano che ThreadLocal è un po 'più lento di un campo normale, ma nello stesso ordine. Aprox 12% più 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);
}
}
}'
Output:
Esempio di campo 0-Running
Esempio di campo 0-End: 6044
Esempio locale di thread in esecuzione 0
Esempio locale thread 0-End: 6015
Esempio di campo a 1 corsa
Esempio di campo a 1 estremità: 5095
Esempio locale di thread in esecuzione 1
Esempio locale di thread a 1 estremità: 5720
Esempio di campo a 2 corse
Esempio di campo a 2 estremità: 4842
Esempio locale di thread in esecuzione 2
Esempio locale di thread a 2 estremità: 5835
Esempio di campo a 3 corse
Esempio di campo a 3 estremità: 4674
Esempio locale di thread in esecuzione 3
Esempio locale di thread a 3 estremità: 5287
Esempio di campo a 4 corse
Esempio di campo a 4 estremità: 4849
Esempio locale di thread in esecuzione 4
Esempio locale di thread a 4 estremità: 5309
Esempio di 5 campi in esecuzione
Esempio di campo a 5 punte: 4781
Esempio locale di thread in esecuzione 5
Esempio locale di thread a 5 estremità: 5330
Esempio di 6 campi in esecuzione
Esempio di campo a 6 punte: 5294
Esempio locale di thread in esecuzione 6
Esempio locale di thread a 6 punte: 5511
Esempio di 7 campi in esecuzione
Esempio di campo a 7 punte: 5119
Esempio locale di thread in esecuzione 7
Esempio locale di thread a 7 estremità: 5793
Esempio di 8 campi in esecuzione
Esempio di campo a 8 punte: 4977
Esempio locale di thread in esecuzione 8
Esempio locale di thread a 8 estremità: 6374
Esempio di 9 campi in esecuzione
Esempio di campo a 9 punte: 4841
Esempio locale di thread in esecuzione 9
Esempio locale di thread a 9 punte: 5471
Field avg: 5051
Discussione media: 5664
Env:
versione openjdk " 1.8.0_131 "
Intel # 174 &; Nucleo # 8482 &; CPU i7-7500U a 2,70 GHz & # 215; 4
Ubuntu 16.04 LTS
@Pete è il test corretto prima di ottimizzare.
Sarei molto sorpreso se la costruzione di un MessageDigest comporta un grave sovraccarico rispetto all'utilizzo effettivo.
Perdere l'utilizzo di ThreadLocal può essere una fonte di perdite e riferimenti penzolanti, che non hanno un ciclo di vita chiaro, in genere non uso mai ThreadLocal senza un piano molto chiaro su quando una particolare risorsa verrà rimossa.
Costruiscilo e misuralo.
Inoltre, è necessario un solo threadlocal se si incapsula il comportamento di digestione dei messaggi in un oggetto. Se per qualche scopo hai bisogno di un MessageDigest locale e di un byte locale [1000], crea un oggetto con un campo messageDigest e un byte [] e inseriscilo in ThreadLocal anziché in entrambi singolarmente.