Wydajność zmiennej ThreadLocal

86

Ile jest odczytywane ze ThreadLocalzmiennej wolniej niż ze zwykłego pola?

Bardziej konkretnie, czy proste tworzenie obiektów jest szybsze czy wolniejsze niż dostęp do ThreadLocalzmiennej?

Zakładam, że jest na tyle szybki, że posiadanie ThreadLocal<MessageDigest>instancji jest znacznie szybsze niż tworzenie instancji za MessageDigestkażdym razem. Ale czy dotyczy to na przykład bajtu [10] lub bajtu [1000]?

Edycja: Pytanie, co się naprawdę dzieje, gdy dzwonisz ThreadLocaldo get? Jeśli to tylko pole, jak każde inne, odpowiedź brzmiałaby: „zawsze jest najszybciej”, prawda?

Sarmun
źródło
2
Lokalny wątek to w zasadzie pole zawierające hashmap i odnośnik, gdzie kluczem jest bieżący obiekt wątku. Jest zatem znacznie wolniejszy, ale nadal szybki. :)
eckes
1
@eckes: z pewnością tak się zachowuje, ale zwykle nie jest implementowane w ten sposób. Zamiast tego Threads zawierają (niezsynchronizowane) mapy hasłowe, w których kluczem jest bieżący ThreadLocalobiekt
sbk

Odpowiedzi:

40

Uruchomienie niepublikowanych testów porównawczych ThreadLocal.getzajmuje około 35 cykli na iterację na moim komputerze. Niezbyt dużo. W implementacji firmy Sun niestandardowa mapa mieszania z sondowaniem liniowym w Threadmapach ThreadLocals na wartości. Ponieważ jest dostępny tylko przez jeden wątek, może być bardzo szybki.

Alokacja małych obiektów zajmuje podobną liczbę cykli, chociaż z powodu wyczerpania pamięci podręcznej można uzyskać nieco niższe wartości w ciasnej pętli.

Budowa MessageDigestprawdopodobnie będzie stosunkowo droga. Ma sporo stanu, a konstrukcja przechodzi przez Providermechanizm SPI. Możesz być w stanie zoptymalizować, na przykład, klonując lub udostępniając Provider.

To, że buforowanie ThreadLocalzamiast tworzenia może być szybsze, niekoniecznie oznacza, że ​​wydajność systemu wzrośnie. Będziesz miał dodatkowe narzuty związane z GC, które spowalniają wszystko.

O ile Twoja aplikacja nie używa bardzo intensywnie, MessageDigestmożesz rozważyć użycie zamiast tego konwencjonalnej pamięci podręcznej bezpiecznej dla wątków.

Tom Hawtin - haczyk
źródło
5
IMHO, najszybszym sposobem jest po prostu zignorowanie SPI i użycie czegoś w rodzaju new org.bouncycastle.crypto.digests.SHA1Digest(). Jestem pewien, że żadna pamięć podręczna nie może tego pokonać.
maaartinus
57

W 2009 roku niektóre maszyny JVM zaimplementowały ThreadLocal przy użyciu niezsynchronizowanej mapy HashMap w obiekcie Thread.currentThread (). To sprawiło, że było to niezwykle szybkie (choć oczywiście nie tak szybkie, jak przy użyciu zwykłego dostępu do pola), a także zapewniło, że obiekt ThreadLocal został uporządkowany, gdy wątek umarł. Aktualizując tę ​​odpowiedź w 2016 r., Wydaje się, że większość (wszystkich?) Nowszych maszyn JVM używa ThreadLocalMap z sondowaniem liniowym. Nie jestem pewien co do ich wydajności - ale nie wyobrażam sobie, że jest znacznie gorsza niż wcześniejsza realizacja.

Oczywiście, nowy Object () jest również obecnie bardzo szybki, a Garbage Collectors są również bardzo dobre w odzyskiwaniu krótkotrwałych obiektów.

O ile nie masz pewności, że tworzenie obiektów będzie kosztowne lub musisz zachować pewien stan w wątku po wątku, lepiej jest wybrać prostsze rozwiązanie alokacji w razie potrzeby i przełączenie się na implementację ThreadLocal tylko wtedy, gdy profiler mówi, że musisz.

Bill Michell
źródło
4
+1 za jedyną odpowiedź, która faktycznie odpowiada na to pytanie.
cletus
Czy możesz podać przykład nowoczesnej maszyny JVM, która nie używa sondowania liniowego dla ThreadLocalMap? Java 8 OpenJDK nadal wydaje się używać ThreadLocalMap z sondowaniem liniowym. grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/...
Karthick
1
@Karthick Przepraszam, nie mogę. Napisałem to w 2009 roku. Zaktualizuję.
Bill Michell
34

Dobre pytanie, zadawałem sobie to ostatnio pytanie. Aby podać konkretne liczby, poniższe testy porównawcze (w Scali, skompilowane do praktycznie tych samych kodów bajtowych, co równoważny kod 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)
}

dostępne tutaj , zostały wykonane na dwurdzeniowych procesorach AMD 4x 2,8 GHz i czterordzeniowych procesorach i7 z hiperwątkowością (2,67 GHz).

Oto liczby:

i7

Specyfikacja: Czterordzeniowy procesor Intel i7 2x @ 2,67 GHz Test: scala.thread.ParallelTests

Nazwa testu: loop_heap_read

Numer wątku: 1 Wszystkich testów: 200

Czasy działania: (pokazuje ostatnie 5) 9,0069 9,0036 9,0017 9,0084 9,0074 (śr. = 9,1034 min = 8,9986 maks. = 21,0306)

Numer wątku: 2 Testy ogółem: 200

Czasy pracy: (pokazuje ostatnie 5) 4,5563 4,7128 4,5663 4,5617 4,5724 (śr. = 4,6337 min = 4,5509 maks. = 13,9476)

Numer wątku: 4 Wszystkich testów: 200

Czasy działania: (pokazuje ostatnie 5) 2,3946 2,3979 2,3934 2,3937 2,3964 (śr. = 2,5113 min = 2,3884 maks. = 13,5496)

Numer wątku: 8 Wszystkich testów: 200

Czasy pracy: (pokazuje ostatnie 5) 2,4479 2,4362 2,4323 2,4472 2,4383 (śr. = 2,5562 min = 2,4166 maks. = 10,3726)

Nazwa testu: threadlocal

Numer wątku: 1 Wszystkich testów: 200

Czasy działania: (pokazuje ostatnie 5) 91,1741 90,8978 90,6181 90,6200 90,6113 (śr. = 91,0291 min = 90,6000 maks. = 129,7501)

Numer wątku: 2 Testy ogółem: 200

Czasy pracy: (pokazuje ostatnie 5) 45,3838 45,3858 45,6676 45,3772 45,3839 (śr. = 46,0555 min = 45,3726 maks. = 90,7108)

Numer wątku: 4 Wszystkich testów: 200

Czasy pracy: (pokazuje ostatnie 5) 22,8118 22,8135 59,1753 22,8229 22,8172 (śr. = 23,9752 min = 22,7951 maks. = 59,1753)

Numer wątku: 8 Wszystkich testów: 200

Czasy pracy: (pokazuje ostatnie 5) 22,2965 22,2415 22,3438 22,3109 22,4460 (śr. = 23,2676 min = 22,2346 maks. = 50,3583)

AMD

Specyfikacja: Dwurdzeniowy procesor AMD 8220 4x @ 2,8 GHz Test: scala.thread.ParallelTests

Nazwa testu: loop_heap_read

Wszystkich prac: 20000000 Numer wątku: 1 Wszystkich testów: 200

Czasy pracy: (pokazuje ostatnie 5) 12,625 12,631 12,634 12,632 12,628 (śr. = 12,7333 min = 12,619 maks. = 26,698)

Nazwa testu: loop_heap_read Całkowita praca: 20000000

Czasy pracy: (pokazuje ostatnie 5) 6,412 6,424 6,408 6,397 6,43 (śr. = 6,5367 min = 6,393 maks. = 19,716)

Numer wątku: 4 Wszystkich testów: 200

Czasy pracy: (pokazuje ostatnie 5) 3,385 4,298 9,7 6,535 3,385 (śr. = 5,6079 min = 3,354 maks. = 21,603)

Numer wątku: 8 Wszystkich testów: 200

Czasy pracy: (pokazuje ostatnie 5) 5,389 5,795 10,818 3,823 3,824 (śr. = 5,5810 min = 2,405 maks. = 19,755)

Nazwa testu: threadlocal

Numer wątku: 1 Wszystkich testów: 200

Czasy pracy: (pokazuje ostatnie 5) 200,217 207,335 200,241 207,342 200,23 (śr. = 202,2424 min = 200,184 maks. = 245,369)

Numer wątku: 2 Testy ogółem: 200

Czasy pracy: (pokazuje ostatnie 5) 100,208 100,199 100,211 103,781 100,215 (śr. = 102,2238 min = 100,192 maks. = 129,505)

Numer wątku: 4 Wszystkich testów: 200

Czasy pracy: (pokazuje ostatnie 5) 62,101 67,629 62,087 52,021 55,766 (śr. = 65,6361 min = 50,282 maks. = 167,433)

Numer wątku: 8 Wszystkich testów: 200

Czasy działania: (pokazuje ostatnie 5) 40,672 74,301 34,434 41,549 28,119 (śr. = 54,7701 min. = 28,119 maks. = 94,424)

Podsumowanie

Wątek lokalny jest około 10–20 razy większy niż odczyt sterty. Wydaje się również, że dobrze skaluje się w tej implementacji JVM i tych architekturach z liczbą procesorów.

axel22
źródło
5
+1 Kudos za bycie jedynym, który daje wyniki ilościowe. Jestem trochę sceptyczny, ponieważ te testy są w Scali, ale jak powiedziałeś, kody bajtowe Javy powinny być podobne ...
Gravity
Dzięki! Ta pętla while daje praktycznie taki sam kod bajtowy, jaki wygenerowałby odpowiedni kod Java. Jednak na różnych maszynach wirtualnych można było zaobserwować różne czasy - zostało to przetestowane na maszynie Sun JVM 1.6.
axel22
Ten kod testu porównawczego nie symuluje dobrego przypadku użycia dla ThreadLocal. W pierwszej metodzie: każdy wątek będzie miał współdzieloną reprezentację w pamięci, ciąg się nie zmienia. W drugiej metodzie porównuje się koszt wyszukiwania z tablicą hashy, gdzie ciąg znaków jest rozłączny między wszystkimi wątkami.
Joelmob
Ciąg nie zmienia się, ale jest odczytywany z pamięci (zapis "!"nigdy nie występuje) w pierwszej metodzie - pierwsza metoda jest efektywnie równoważna z podklasą Threadi nadaniem jej własnego pola. Benchmark mierzy skrajny przypadek skrajny, w którym całe obliczenia składają się z odczytu zmiennej / wątku lokalnego - w zależności od ich wzorca dostępu może nie mieć to wpływu na rzeczywiste aplikacje, ale w najgorszym przypadku będą one zachowywać się jak powyżej.
axel22
4

Oto kolejny test. Wyniki pokazują, że ThreadLocal jest nieco wolniejszy niż zwykłe pole, ale w tej samej kolejności. Około 12% wolniej

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

Wynik:

0-próbka terenowa

Próbka pola 0-End: 6044

0-uruchomiona próbka lokalna wątku

Próbka lokalna gwintu 0-końca: 6015

1-próbka terenowa

Próbka z 1-końcowym polem: 5095

1-uruchomiony wątek, próbka lokalna

Próbka lokalna gwintu z jednym końcem: 5720

2-biegowa próbka terenowa

Próbka z 2 końcówkami: 4842

2-uruchomiona próbka lokalna wątku

Próbka lokalna z gwintem 2-końcowym: 5835

3-próbka terenowa

Próbka pola z 3 końcówkami: 4674

Próbka lokalna z 3 uruchomionym wątkiem

Próbka lokalna z gwintem 3-końcowym: 5287

4-biegowa próbka terenowa

Próbka z 4 końcówkami: 4849

Próbka lokalna 4-uruchomionego wątku

Próbka lokalna z gwintem 4-końcowym: 5309

5-próbka terenowa

Próbka pola 5-końcowego: 4781

5-uruchomiona próbka lokalna wątku

Próbka lokalna z 5-końcowym gwintem: 5330

6-próbka terenowa

Próbka pola 6-End: 5294

6-uruchomiony wątek lokalny przykład

Próbka lokalna z gwintem 6-końcowym: 5511

7-próbka terenowa

Próbka pola 7-End: 5119

7-uruchomiony wątek lokalnej próbki

Próbka lokalna wątku 7-końcowego: 5793

8-próbka terenowa

Próbka pola 8-End: 4977

8-uruchomiony wątek lokalny przykład

Próbka lokalna z 8-końcowym gwintem: 6374

9-próbka terenowa

Próbka pola 9-End: 4841

9-uruchomiona próbka lokalna wątku

Próbka lokalna z gwintem 9-końcowym: 5471

Średnia z pola: 5051

ThreadLocal avg: 5664

Koperta:

wersja openjdk „1.8.0_131”

Procesor Intel® Core ™ i7-7500U przy 2,70 GHz × 4

Ubuntu 16.04 LTS

jpereira
źródło
Przepraszamy, to nie jest nawet bliskie bycia ważnym testem. A) Największy problem: przydzielasz ciągi znaków w każdej iteracji ( Int.toString)co jest niezwykle kosztowne w porównaniu z tym, co testujesz. B) wykonujesz dwie operacje na mapie w każdej iteracji, również całkowicie niepowiązane i drogie. Spróbuj zamiast tego zwiększyć prymitywny int z ThreadLocal. C) stosowanie System.nanoTimezamiast System.currentTimeMillis, ta pierwsza jest do profilowania, drugi jest dla użytkowników celach Date-Time i mogą ulec zmianie pod swoimi stopami. D) Powinieneś całkowicie unikać alokacji, w tym tych najwyższego poziomu na swoje „przykładowe” zajęcia
Philip Guin
3

@Pete to poprawny test przed optymalizacją.

Byłbym bardzo zaskoczony, gdyby konstruowanie MessageDigest wiązało się z jakimś poważnym narzutem w porównaniu z jego aktywnym użyciem.

Pominięcie korzystania z ThreadLocal może być źródłem wycieków i wiszących odniesień, które nie mają jasnego cyklu życia, generalnie nigdy nie używam ThreadLocal bez bardzo jasnego planu, kiedy dany zasób zostanie usunięty.

Gareth Davis
źródło
0

Zbuduj to i zmierz.

Ponadto potrzebujesz tylko jednego wątku lokalnego, jeśli umieścisz zachowanie przetwarzania wiadomości w obiekcie. Jeśli do jakiegoś celu potrzebujesz lokalnego MessageDigest i lokalnego bajtu [1000], utwórz obiekt z polem messageDigest i bajtem [] i umieść ten obiekt w ThreadLocal zamiast obu osobno.

Pete Kirkham
źródło
Dzięki, MessageDigest i byte [] to różne zastosowania, więc jeden obiekt nie jest potrzebny.
Sarmun