Leistung der ThreadLocal-Variablen

85

Wie viel wird von ThreadLocalVariablen langsamer gelesen als von regulären Feldern?

Konkreter ist die einfache Objekterstellung schneller oder langsamer als der Zugriff auf ThreadLocalVariablen?

Ich gehe davon aus, dass es schnell genug ist, so dass es ThreadLocal<MessageDigest>viel schneller ist, eine Instanz zu haben, als MessageDigestjedes Mal eine Instanz zu erstellen . Aber gilt das zum Beispiel auch für Byte [10] oder Byte [1000]?

Edit: Frage ist, was wirklich los ist, wenn wir anrufen ThreadLocal? Wenn das nur ein Feld ist, wie jedes andere, dann wäre die Antwort "es ist immer am schnellsten", oder?

Sarmun
quelle
2
Ein Thread-Local ist im Grunde ein Feld, das eine Hashmap und eine Suche enthält, wobei der Schlüssel das aktuelle Thread-Objekt ist. Es ist daher viel langsamer aber immer noch schnell. :)
eckes
1
@eckes: es verhält sich sicherlich so, aber es wird normalerweise nicht so implementiert. Stattdessen Threadenthalten s eine (nicht synchronisierte) Hashmap, in der der Schlüssel das aktuelle ThreadLocalObjekt ist
sbk

Antworten:

40

Das Ausführen unveröffentlichter Benchmarks ThreadLocal.getdauert auf meinem Computer ungefähr 35 Zyklen pro Iteration. Nicht viel. In der Implementierung von Sun wird eine benutzerdefinierte lineare Prüf-Hash-Karte in ThreadKarten ThreadLocals auf Werte abgebildet. Da nur ein einzelner Thread darauf zugreift, kann es sehr schnell gehen.

Die Zuordnung kleiner Objekte dauert ähnlich viele Zyklen, obwohl Sie aufgrund der Cache-Erschöpfung in einer engen Schleife möglicherweise etwas niedrigere Zahlen erhalten.

Der Bau von MessageDigestist wahrscheinlich relativ teuer. Es hat eine ganze Menge Staat und der Bau geht durch den ProviderSPI-Mechanismus. Möglicherweise können Sie die Optimierung durchführen, indem Sie beispielsweise die Daten klonen oder bereitstellen Provider.

Nur weil das Zwischenspeichern in einem ThreadLocalSystem möglicherweise schneller ist als das Erstellen, bedeutet dies nicht zwangsläufig, dass die Systemleistung steigt. Sie haben zusätzliche Gemeinkosten im Zusammenhang mit GC, die alles verlangsamen.

Sofern Ihre Anwendung nicht sehr häufig verwendet wird, sollten MessageDigestSie stattdessen einen herkömmlichen thread-sicheren Cache verwenden.

Tom Hawtin - Tackline
quelle
5
IMHO ist der schnellste Weg, einfach den SPI zu ignorieren und so etwas zu verwenden new org.bouncycastle.crypto.digests.SHA1Digest(). Ich bin mir ziemlich sicher, dass kein Cache es schlagen kann.
Maaartinus
57

Im Jahr 2009 implementierten einige JVMs ThreadLocal mithilfe einer nicht synchronisierten HashMap im Thread.currentThread () -Objekt. Dies machte es extrem schnell (wenn auch nicht annähernd so schnell wie die Verwendung eines normalen Feldzugriffs) und stellte sicher, dass das ThreadLocal-Objekt aufgeräumt wurde, als der Thread starb. Bei der Aktualisierung dieser Antwort im Jahr 2016 scheinen die meisten (alle?) Neueren JVMs eine ThreadLocalMap mit linearer Prüfung zu verwenden. Ich bin mir über die Leistung dieser nicht sicher - aber ich kann mir nicht vorstellen, dass sie wesentlich schlechter ist als die frühere Implementierung.

Natürlich ist new Object () heutzutage auch sehr schnell, und die Garbage Collectors sind auch sehr gut darin, kurzlebige Objekte zurückzugewinnen.

Wenn Sie nicht sicher sind, dass die Objekterstellung teuer sein wird, oder wenn Sie einen Status auf Thread-für-Thread-Basis beibehalten müssen, sollten Sie sich für die einfachere Zuweisung bei Bedarf entscheiden und erst dann zu einer ThreadLocal-Implementierung wechseln, wenn a Der Profiler sagt Ihnen, dass Sie müssen.

Bill Michell
quelle
4
+1 für die einzige Antwort, die die Frage tatsächlich beantwortet.
Cletus
Können Sie mir ein Beispiel für eine moderne JVM geben, die keine lineare Prüfung für ThreadLocalMap verwendet? Java 8 OpenJDK scheint weiterhin ThreadLocalMap mit linearer Prüfung zu verwenden. grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/…
Karthick
1
@Karthick Sorry nein ich kann nicht. Ich habe das 2009 geschrieben. Ich werde es aktualisieren.
Bill Michell
34

Gute Frage, das habe ich mir kürzlich gestellt. Um Ihnen eindeutige Zahlen zu geben, die folgenden Benchmarks (in Scala mit praktisch denselben Bytecodes wie der entsprechende Java-Code kompiliert):

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

hier verfügbar , wurden auf einem AMD 4x 2,8 GHz Dual-Cores und einem Quad-Core i7 mit Hyperthreading (2,67 GHz) durchgeführt.

Das sind die Zahlen:

i7

Technische Daten: Intel i7 2x Quad-Core bei 2,67 GHz Test: scala.threads.ParallelTests

Testname: loop_heap_read

Fadennummer: 1 Gesamttests: 200

Laufzeiten: (zeigt die letzten 5 an) 9.0069 9.0036 9.0017 9.0084 9.0074 (Durchschnitt = 9.1034 min = 8.9986 max = 21.0306)

Fadennummer: 2 Gesamtprüfungen: 200

Laufzeiten: (zeigt die letzten 5) 4.5563 4.7128 4.5663 4.5617 4.5724 (Durchschnitt = 4.6337 min = 4.5509 max = 13.9476)

Fadennummer: 4 Gesamttests: 200

Laufzeiten: (zeigt die letzten 5) 2.3946 2.3979 2.3934 2.3937 2.3964 (Durchschnitt = 2.5113 min = 2.3884 max = 13.5496)

Fadennummer: 8 Gesamttests: 200

Laufzeiten: (zeigt die letzten 5) 2.4479 2.4362 2.4323 2.4472 2.4383 (Durchschnitt = 2.5562 min = 2.4166 max = 10.3726)

Testname: threadlocal

Fadennummer: 1 Gesamttests: 200

Laufzeiten: (zeigt die letzten 5) 91.1741 90.8978 90.6181 90.6200 90.6113 (Durchschnitt = 91.0291 min = 90.6000 max = 129.7501)

Fadennummer: 2 Gesamtprüfungen: 200

Laufzeiten: (zeigt die letzten 5) 45.3838 45.3858 45.6676 45.3772 45.3839 (Durchschnitt = 46.0555 min = 45.3726 max = 90.7108)

Fadennummer: 4 Gesamttests: 200

Laufzeiten: (zeigt die letzten 5) 22.8118 22.8135 59.1753 22.8229 22.8172 (Durchschnitt = 23.9752 min = 22.7951 max = 59.1753)

Fadennummer: 8 Gesamttests: 200

Laufzeiten: (zeigt die letzten 5) 22.2965 22.2415 22.3438 22.3109 22.4460 (Durchschnitt = 23.2676 min = 22.2346 max = 50.3583)

AMD

Technische Daten: AMD 8220 4x Dual-Core bei 2,8 GHz Test: scala.threads.ParallelTests

Testname: loop_heap_read

Gesamtarbeit: 20000000 Fadennummer: 1 Gesamtprüfungen: 200

Laufzeiten: (zeigt die letzten 5) 12.625 12.631 12.634 12.632 12.628 (Durchschnitt = 12.7333 min = 12.619 max = 26.698)

Testname: loop_heap_read Gesamtarbeit: 20000000

Laufzeiten: (zeigt die letzten 5) 6.412 6.424 6.408 6.397 6.43 (Durchschnitt = 6.5367 min = 6.393 max = 19.716)

Fadennummer: 4 Gesamttests: 200

Laufzeiten: (zeigt die letzten 5) 3.385 4.298 9.7 6.535 3.385 (Durchschnitt = 5.6079 min = 3.354 max = 21.603)

Fadennummer: 8 Gesamttests: 200

Laufzeiten: (zeigt die letzten 5) 5.389 5.795 10.818 3.823 3.824 (Durchschnitt = 5.5810 min = 2.405 max = 19.755)

Testname: threadlocal

Fadennummer: 1 Gesamttests: 200

Laufzeiten: (zeigt die letzten 5 an) 200,217 207,335 200,241 207,342 200,23 (Durchschnitt = 202,2424 min = 200,184 max = 245,369)

Fadennummer: 2 Gesamtprüfungen: 200

Laufzeiten: (zeigt die letzten 5) 100.208 100.199 100.211 103.781 100.215 (Durchschnitt = 102.2238 min = 100.192 max = 129.505)

Fadennummer: 4 Gesamttests: 200

Laufzeiten: (zeigt die letzten 5) 62.101 67.629 62.087 52.021 55.766 (Durchschnitt = 65.6361 min = 50.282 max = 167.433)

Fadennummer: 8 Gesamttests: 200

Laufzeiten: (zeigt die letzten 5) 40.672 74.301 34.434 41.549 28.119 (Durchschnitt = 54.7701 min = 28.119 max = 94.424)

Zusammenfassung

Ein lokaler Thread ist ungefähr 10-20x so groß wie der des gelesenen Heaps. Es scheint auch gut auf diese JVM-Implementierung und diese Architekturen mit der Anzahl der Prozessoren zu skalieren.

axel22
quelle
5
+1 Ein großes Lob dafür, dass Sie als einziger quantitative Ergebnisse liefern. Ich bin ein bisschen skeptisch, weil diese Tests in Scala sind, aber wie Sie sagten, sollten die Java-Bytecodes ähnlich sein ...
Schwerkraft
Vielen Dank! Diese while-Schleife führt zu praktisch demselben Bytecode, den der entsprechende Java-Code erzeugen würde. Auf verschiedenen VMs konnten jedoch unterschiedliche Zeiten beobachtet werden - dies wurde auf einer Sun JVM1.6 getestet.
Axel22
Dieser Benchmark-Code simuliert keinen guten Anwendungsfall für ThreadLocal. Bei der ersten Methode: Jeder Thread hat eine gemeinsame Darstellung im Speicher, die Zeichenfolge ändert sich nicht. Bei der zweiten Methode vergleichen Sie die Kosten einer Hashtabellensuche, bei der die Zeichenfolge zwischen allen Threads disjunktiv ist.
Joelmob
Die Zeichenfolge ändert sich nicht, wird jedoch "!"in der ersten Methode aus dem Speicher gelesen (das Schreiben von erfolgt nie). Die erste Methode entspricht praktisch der Unterklasse Threadund dem Zuweisen eines benutzerdefinierten Felds. Der Benchmark misst einen extremen Randfall, bei dem die gesamte Berechnung aus dem Lesen einer Variablen / eines lokalen Threads besteht. Reale Anwendungen sind möglicherweise nicht von ihrem Zugriffsmuster betroffen, verhalten sich jedoch im schlimmsten Fall wie oben.
Axel22
4

Hier geht es noch einen Test. Die Ergebnisse zeigen, dass ThreadLocal etwas langsamer als ein reguläres Feld ist, jedoch in derselben Reihenfolge. Ca. 12% langsamer

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

Ausgabe:

0-laufendes Feldbeispiel

0-End Feldprobe: 6044

0-laufendes lokales Thread-Beispiel

Lokales Beispiel für 0-End-Thread: 6015

1-Lauffeldprobe

1-End-Feldprobe: 5095

1-laufendes lokales Thread-Beispiel

Lokales Beispiel für 1-End-Thread: 5720

2-Lauffeldprobe

2-End-Feldprobe: 4842

2-laufendes lokales Thread-Beispiel

Lokales Beispiel für ein 2-End-Gewinde: 5835

3-Lauffeldprobe

3-End-Feldprobe: 4674

3-laufendes lokales Thread-Beispiel

Lokales 3-End-Thread-Beispiel: 5287

4-Lauffeldprobe

4-End-Feldprobe: 4849

4-laufendes lokales Thread-Beispiel

Lokales 4-End-Thread-Beispiel: 5309

5-Lauffeldprobe

5-End-Feldprobe: 4781

5-Ausführen eines lokalen Thread-Beispiels

Lokales 5-End-Thread-Beispiel: 5330

6-Lauffeldprobe

6-End-Feldprobe: 5294

6-Ausführen eines lokalen Thread-Beispiels

Lokales 6-End-Thread-Beispiel: 5511

7-Lauffeldprobe

7-End-Feldprobe: 5119

7-Ausführen eines lokalen Thread-Beispiels

Lokales 7-End-Thread-Beispiel: 5793

8-Lauffeldprobe

8-End-Feldprobe: 4977

8-laufendes lokales Thread-Beispiel

Lokales Beispiel für 8-End-Thread: 6374

9-Lauffeldprobe

9-End Feldprobe: 4841

9-Ausführen eines lokalen Thread-Beispiels

Lokales 9-End-Thread-Beispiel: 5471

Felddurchschn.: 5051

ThreadLocal-Durchschnitt: 5664

Env:

openjdk version "1.8.0_131"

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

Ubuntu 16.04 LTS

jpereira
quelle
Entschuldigung, dies ist nicht einmal annähernd ein gültiger Test. A) Größtes Problem: Sie weisen jeder Iteration Strings zu ( Int.toString)was im Vergleich zu dem, was Sie testen, extrem teuer ist. B) Sie führen bei jeder Iteration zwei Map-Ops durch, auch völlig unabhängig und teuer. Versuchen Sie stattdessen, ein primitives int aus ThreadLocal zu erhöhen. C) Verwenden Sie System.nanoTimestatt System.currentTimeMillis, das erstere dient der Profilerstellung, das letztere dient der Datums- und Uhrzeit des Benutzers und kann sich unter Ihren Füßen ändern. D) Sie sollten Zuweisungen vollständig vermeiden, einschließlich der Zuweisungen der obersten Ebene für Ihre "Beispiel" -Klassen
Philip Guin
3

@Pete ist korrekter Test, bevor Sie optimieren.

Ich wäre sehr überrascht, wenn das Erstellen eines MessageDigest im Vergleich zur tatsächlichen Verwendung einen ernsthaften Overhead hätte.

Die fehlende Verwendung von ThreadLocal kann eine Quelle für Lecks und baumelnde Referenzen sein, die keinen eindeutigen Lebenszyklus haben. Im Allgemeinen verwende ich ThreadLocal nie ohne einen sehr klaren Plan, wann eine bestimmte Ressource entfernt wird.

Gareth Davis
quelle
0

Bauen Sie es und messen Sie es.

Außerdem benötigen Sie nur einen Threadlocal, wenn Sie Ihr Nachrichtenverdauungsverhalten in ein Objekt einkapseln. Wenn Sie für einen bestimmten Zweck ein lokales MessageDigest und ein lokales Byte [1000] benötigen, erstellen Sie ein Objekt mit einem messageDigest- und einem Byte [] -Feld und fügen Sie dieses Objekt in den ThreadLocal ein, anstatt beide einzeln.

Pete Kirkham
quelle
Dank, MessageDigest und byte [] werden unterschiedlich verwendet, sodass kein Objekt benötigt wird.
Sarmun