Prestazioni della variabile ThreadLocal


87

Quanto viene letto dalla ThreadLocalvariabile più lentamente rispetto al campo normale?

Più concretamente, la creazione di un semplice oggetto è più veloce o più lenta dell'accesso alla ThreadLocalvariabile?

Presumo che sia abbastanza veloce in modo che avere ThreadLocal<MessageDigest>istanza sia molto più veloce della creazione di istanze di MessageDigestogni volta. Ma questo vale anche per byte [10] o byte [1000], ad esempio?

Modifica: la domanda è cosa sta realmente accadendo quando si chiama ThreadLocalget? Se quello è solo un campo, come tutti gli altri, la risposta sarebbe "è sempre più veloce", giusto?


2
Un thread locale è fondamentalmente un campo contenente una hashmap e una ricerca in cui la chiave è l'oggetto thread corrente. È quindi molto più lento ma comunque veloce. :)
eckes il

1
@eckes: certamente si comporta così, ma di solito non è implementato in questo modo. Invece, Threads contengono una hashmap (non sincronizzata) in cui la chiave è l' ThreadLocaloggetto corrente
sbk

Risposte:


40

L'esecuzione di benchmark non pubblicati ThreadLocal.getrichiede circa 35 cicli per iterazione sulla mia macchina. Non molto. Nell'implementazione di Sun, una mappa hash di sondaggio lineare personalizzato nelle Threadmappe ThreadLocals ai valori. Poiché è accessibile solo da un singolo thread, può essere molto veloce.

L'assegnazione di piccoli oggetti richiede un numero simile di cicli, sebbene a causa dell'esaurimento della cache si possano ottenere cifre leggermente inferiori in un ciclo ristretto.

Costruzione di MessageDigestè probabile che sia relativamente costoso. Ha una discreta quantità di stato e la costruzione passa attraverso il Providermeccanismo SPI. Potresti essere in grado di ottimizzare, ad esempio, clonando o fornendo il file Provider.

Solo perché potrebbe essere più veloce memorizzare nella cache in un file ThreadLocal piuttosto che creare non significa necessariamente che le prestazioni del sistema aumenteranno. Avrai costi aggiuntivi relativi a GC che rallenta tutto.

A meno che la tua applicazione non usi molto MessageDigest, potresti prendere in considerazione l'utilizzo di una cache thread-safe convenzionale.


5
IMHO, il modo più veloce è semplicemente ignorare lo SPI e usare qualcosa di simile new org.bouncycastle.crypto.digests.SHA1Digest(). Sono abbastanza sicuro che nessuna cache può batterlo.
maaartinus

57

Nel 2009, alcuni JVM implementato ThreadLocalutilizzando un sincronismo HashMapin Thread.currentThread()oggetto. Ciò lo rese estremamente veloce (anche se non così veloce come usare un normale accesso al campo, ovviamente), oltre a garantire che l' ThreadLocaloggetto fosse riordinato quando Threadmoriva. Aggiornando questa risposta nel 2016, sembra che la maggior parte (tutte?) Le JVM più recenti utilizzino un fileThreadLocalMap con rilevamento lineare. Non sono sicuro delle prestazioni di questi, ma non riesco a immaginare che sia significativamente peggiore dell'implementazione precedente.

Ovviamente, new Object() è anche molto veloce in questi giorni, e i netturbini sono anche molto bravi a recuperare oggetti di breve durata.

A meno che tu non sia certo che la creazione di oggetti sarà costosa, o che tu abbia bisogno di mantenere uno stato thread per thread, è meglio optare per la soluzione più semplice allocare quando necessario e passare a ThreadLocalun'implementazione solo quando un profiler ti dice che devi.


4
+1 per essere l'unica risposta per rispondere effettivamente alla domanda.
cletus

Potete farmi un esempio di una moderna JVM che non utilizza il rilevamento lineare per ThreadLocalMap? Java 8 OpenJDK sembra ancora utilizzare ThreadLocalMap con il rilevamento lineare. grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/…
Karthick

1
@ Karthick Sorry no non posso. L'ho scritto nel 2009. Aggiornerò.
Bill Michell

34

Bella domanda, me lo sono chiesto di recente. Per darti numeri precisi, i benchmark di seguito (in Scala, compilati praticamente con gli stessi bytecode del codice 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)
}

disponibili qui , sono stati eseguiti su un dual-core AMD 4x 2.8 GHz e un i7 quad-core con hyperthreading (2.67 GHz).

Questi i numeri:

i7

Specifiche: Intel i7 2x quad-core a 2,67 GHz Test: scala.threads.ParallelTests

Nome del test: loop_heap_read

Numero thread: 1 Test totali: 200

Tempi di esecuzione: (mostrando gli ultimi 5) 9.0069 9.0036 9.0017 9.0084 9.0074 (avg = 9.1034 min = 8.9986 max = 21.0306)

Num. Filetto: 2 Test totali: 200

Tempi di esecuzione: (mostrando gli ultimi 5) 4.5563 4.7128 4.5663 4.5617 4.5724 (avg = 4.6337 min = 4.5509 max = 13.9476)

Num. Filetto: 4 Test totali: 200

Tempi di esecuzione: (mostrando gli ultimi 5) 2.3946 2.3979 2.3934 2.3937 2.3964 (avg = 2.5113 min = 2.3884 max = 13.5496)

Numero thread: 8 Test totali: 200

Tempi di esecuzione: (mostrando gli ultimi 5) 2.4479 2.4362 2.4323 2.4472 2.4383 (avg = 2.5562 min = 2.4166 max = 10.3726)

Nome del test: threadlocal

Numero thread: 1 Test totali: 200

Tempi di esecuzione: (mostrando gli ultimi 5) 91.1741 90.8978 90.6181 90.6200 90.6113 (avg = 91.0291 min = 90.6000 max = 129.7501)

Num. Filetto: 2 Test totali: 200

Tempi di esecuzione: (mostrando gli ultimi 5) 45.3838 45.3858 45.6676 45.3772 45.3839 (avg = 46.0555 min = 45.3726 max = 90.7108)

Num. Filetto: 4 Test totali: 200

Tempi di esecuzione: (mostrando gli ultimi 5) 22,8118 22,8135 59,1753 22,8229 22,8172 (avg = 23,9752 min = 22,7951 max = 59,1753)

Numero thread: 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 del test: loop_heap_read

Lavoro totale: 20000000 Num. Filetto: 1 Test totali: 200

Tempi di esecuzione: (mostrando gli ultimi 5) 12.625 12.631 12.634 12.632 12.628 (avg = 12.7333 min = 12.619 max = 26.698)

Nome del test: loop_heap_read Lavoro totale: 20000000

Tempi di esecuzione: (mostrando gli ultimi 5) 6.412 6.424 6.408 6.397 6.43 (avg = 6.5367 min = 6.393 max = 19.716)

Num. Filetto: 4 Test totali: 200

Tempi di esecuzione: (mostrando gli ultimi 5) 3.385 4.298 9.7 6.535 3.385 (avg = 5.6079 min = 3.354 max = 21.603)

Numero thread: 8 Test totali: 200

Tempi di esecuzione: (mostrando gli ultimi 5) 5.389 5.795 10.818 3.823 3.824 (avg = 5.5810 min = 2.405 max = 19.755)

Nome del test: threadlocal

Numero thread: 1 Test totali: 200

Tempi di esecuzione: (mostrando gli ultimi 5) 200,217 207,335 200,241 207,342 200,23 (avg = 202,2424 min = 200,184 max = 245,369)

Num. Filetto: 2 Test totali: 200

Tempi di esecuzione: (mostrando gli ultimi 5) 100.208 100.199 100.211 103.781 100.215 (avg = 102.2238 min = 100.192 max = 129.505)

Num. Filetto: 4 Test totali: 200

Tempi di esecuzione: (mostrando gli ultimi 5) 62.101 67.629 62.087 52.021 55.766 (avg = 65.6361 min = 50.282 max = 167.433)

Numero thread: 8 Test totali: 200

Tempi di esecuzione: (mostrando gli ultimi 5) 40.672 74.301 34.434 41.549 28.119 (avg = 54.7701 min = 28.119 max = 94.424)

Sommario

Un thread locale è circa 10-20 volte quello dell'heap letto. Sembra anche scalare bene su questa implementazione JVM e queste architetture con il numero di processori.


5
+1 Complimenti per essere l'unico a dare risultati quantitativi. Sono un po 'scettico perché questi test sono in Scala, ma come hai detto, i bytecode Java dovrebbero essere simili ...
Gravity

Grazie! Questo ciclo while produce virtualmente lo stesso bytecode del codice Java corrispondente. Tempi diversi potrebbero essere osservati su diverse VM, tuttavia: questo è stato testato su una Sun JVM1.6.
axel22

Questo codice di benchmark non simula un buon caso d'uso per ThreadLocal. Nel primo metodo: ogni thread avrà una rappresentazione condivisa in memoria, la stringa non cambia. Nel secondo metodo si confronta il costo di una ricerca in tabella hash in cui la stringa è disgiuntiva tra tutti i thread.
Joelmob

La stringa non cambia, ma viene letta dalla memoria (la scrittura di "!"non si verifica mai) nel primo metodo: il primo metodo è effettivamente equivalente alla sottoclasse Threade assegnandogli un campo personalizzato. Il benchmark misura un caso limite estremo in cui l'intero calcolo consiste nella lettura di una variabile / thread locale: le applicazioni reali potrebbero non essere influenzate a seconda del loro modello di accesso, ma nel peggiore dei casi si comporteranno come sopra.
axel22

4

Ecco un altro test. I risultati mostrano che ThreadLocal è un po 'più lento di un campo normale, ma nello stesso ordine. Circa il 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);
    }
}
}'

Produzione:

0-Campione di campo in esecuzione

0-Campione del campo finale: 6044

0-Esecuzione di esempio locale del thread

Esempio locale del thread 0-End: 6015

1-Campione di campo in esecuzione

Campione di campo 1-End: 5095

1-Esecuzione di esempio locale di thread

Esempio locale di thread 1-End: 5720

2-Running field sample

Campione di campo 2-End: 4842

2-Esecuzione di esempio locale di thread

Esempio locale di thread a 2 estremità: 5835

3-Running field sample

Campione di campo 3-End: 4674

3-Esecuzione di esempio locale di thread

Esempio locale di thread a 3 estremità: 5287

4-Running field sample

Campione di campo a 4 estremità: 4849

4-Esecuzione di esempio locale di thread

Esempio locale di thread a 4 estremità: 5309

5-Campione di campo in esecuzione

5-Campione del campo finale: 4781

5-Esecuzione di esempio locale di thread

Esempio locale di thread 5-End: 5330

6-Campione di campo in esecuzione

6-Campione campo finale: 5294

6-Esecuzione di esempio locale di thread

Esempio locale di thread 6-End: 5511

7-Campione di campo in esecuzione

7-Campione del campo finale: 5119

7-Esecuzione di esempio locale di thread

Esempio locale di thread 7-End: 5793

8-Campione di campo in esecuzione

8-Campione campo finale: 4977

8-Esecuzione di esempio locale di thread

Esempio locale di thread 8-End: 6374

9-Campione di campo in esecuzione

9-Campione campo finale: 4841

9-Esecuzione di esempio locale di thread

Esempio locale di thread 9-End: 5471

Media campo: 5051

Media Locale Discussione: 5664

Env:

openjdk versione "1.8.0_131"

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

Ubuntu 16.04 LTS


1
Spiacenti, questo non è nemmeno vicino a essere un test valido. A) Il problema più grande: stai allocando stringhe ad ogni iterazione ( Int.toString), che è estremamente costoso rispetto a quello che stai testando. B) stai facendo due operazioni di mappa ogni iterazione, anche totalmente estranee e costose. Prova invece a incrementare un int primitivo da ThreadLocal. C) Utilizzo al System.nanoTimeposto di System.currentTimeMillis, il primo è per la profilazione, il secondo per finalità data-ora dell'utente e può variare sotto i tuoi piedi. D) Dovresti evitare del tutto gli alloc, compresi quelli di primo livello per le tue classi di "esempio"
Philip Guin

3

@Pete è il test corretto prima di ottimizzare.

Sarei molto sorpreso se la costruzione di un MessageDigest avesse un sovraccarico serio rispetto al suo utilizzo effettivo.

Non usare ThreadLocal può essere una fonte di perdite e riferimenti penzolanti, che non hanno un ciclo di vita chiaro, generalmente non uso mai ThreadLocal senza un piano molto chiaro di quando una particolare risorsa verrà rimossa.


0

Costruiscilo e misuralo.

Inoltre, è necessario un solo threadlocal se si incapsula il comportamento di digestione dei messaggi in un oggetto. Se hai bisogno di un MessageDigest locale e di un byte locale [1000] per qualche scopo, crea un oggetto con un campo messageDigest e un byte [] e metti quell'oggetto in ThreadLocal piuttosto che entrambi singolarmente.


Grazie, MessageDigest e byte [] sono usi diversi, quindi non è necessario un oggetto.
Sarmun
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.