Kinerja variabel ThreadLocal

87

Berapa banyak yang dibaca dari ThreadLocalvariabel lebih lambat daripada dari bidang biasa?

Lebih konkretnya apakah pembuatan objek sederhana lebih cepat atau lebih lambat daripada akses ke ThreadLocalvariabel?

Saya berasumsi bahwa ini cukup cepat sehingga memiliki ThreadLocal<MessageDigest>instance jauh lebih cepat daripada membuat instance MessageDigestsetiap saat. Tetapi apakah itu juga berlaku untuk byte [10] atau byte [1000] misalnya?

Edit: Pertanyaan apa yang sebenarnya terjadi saat panggilan ThreadLocalget? Jika itu hanya sebuah bidang, seperti bidang lainnya, maka jawabannya adalah "selalu tercepat", bukan?

Sarmun
sumber
2
Lokal utas pada dasarnya adalah bidang yang berisi peta hash dan pencarian dengan kuncinya adalah objek utas saat ini. Oleh karena itu, jauh lebih lambat tetapi tetap cepat. :)
eckes
1
@eckes: memang berperilaku seperti itu, tetapi biasanya tidak diterapkan dengan cara ini. Sebagai gantinya, Threads berisi hashmap (tidak tersinkronisasi) di mana kuncinya adalah ThreadLocalobjek saat ini
sbk

Jawaban:

40

Menjalankan tolok ukur yang tidak dipublikasikan, ThreadLocal.getmembutuhkan sekitar 35 siklus per iterasi di mesin saya. Tidak banyak. Dalam implementasi Sun, peta hash probing linier kustom di Threadpeta ThreadLocals ke nilai. Karena hanya dapat diakses oleh satu utas, itu bisa sangat cepat.

Alokasi objek kecil mengambil jumlah siklus yang sama, meskipun karena kehabisan cache Anda mungkin mendapatkan angka yang lebih rendah dalam loop yang ketat.

Konstruksi MessageDigestakan relatif mahal. Ini memiliki jumlah yang adil dan konstruksi melalui Providermekanisme SPI. Anda mungkin dapat mengoptimalkan, misalnya, menggandakan atau menyediakan Provider.

Hanya karena mungkin lebih cepat untuk menyimpan dalam cache ThreadLocaldaripada membuat tidak selalu berarti bahwa kinerja sistem akan meningkat. Anda akan memiliki biaya tambahan terkait GC yang memperlambat semuanya.

Kecuali jika aplikasi Anda sangat banyak digunakan, MessageDigestAnda mungkin ingin mempertimbangkan untuk menggunakan cache aman-thread konvensional.

Tom Hawtin - tackline
sumber
5
IMHO, cara tercepat adalah mengabaikan SPI dan menggunakan sesuatu seperti itu new org.bouncycastle.crypto.digests.SHA1Digest(). Saya yakin tidak ada cache yang bisa mengalahkannya.
maaartinus
57

Pada tahun 2009, beberapa JVMs diimplementasikan ThreadLocalmenggunakan unsynchronised HashMapdi Thread.currentThread()objek. Ini membuatnya sangat cepat (meskipun tidak secepat menggunakan akses lapangan biasa, tentu saja), serta memastikan bahwa ThreadLocalobjek dirapikan saat Threadmati. Memperbarui jawaban ini pada tahun 2016, tampaknya sebagian besar (semua?) JVM yang lebih baru menggunakan a ThreadLocalMapwith linear probing. Saya tidak yakin tentang kinerja mereka - tetapi saya tidak dapat membayangkan ini jauh lebih buruk daripada penerapan sebelumnya.

Tentu saja, hari new Object()ini juga sangat cepat, dan para pemulung juga sangat pandai mengambil kembali benda-benda yang berumur pendek.

Kecuali jika Anda yakin bahwa pembuatan objek akan mahal, atau Anda perlu mempertahankan beberapa status berdasarkan utas demi utas, lebih baik Anda menggunakan solusi alokasi yang lebih sederhana saat diperlukan, dan hanya beralih ke ThreadLocalimplementasi saat profiler memberi tahu Anda bahwa Anda perlu.

Bill Michell
sumber
4
+1 untuk menjadi satu-satunya jawaban yang benar-benar menjawab pertanyaan tersebut.
cletus
Dapatkah Anda memberi saya contoh JVM modern yang tidak menggunakan probing linier untuk ThreadLocalMap? Java 8 OpenJDK tampaknya masih menggunakan ThreadLocalMap dengan probing linier. grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/…
Karthick
1
@Maaf, tidak, saya tidak bisa. Saya menulis ini kembali pada tahun 2009. Saya akan memperbarui.
Bill Michell
34

Pertanyaan bagus, saya sudah menanyakan hal itu pada diri saya akhir-akhir ini. Untuk memberi Anda angka pasti, tolok ukur di bawah ini (di Scala, dikompilasi menjadi bytecode yang hampir sama dengan kode Java yang setara):

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

tersedia di sini , dilakukan pada AMD 4x 2.8 GHz dual-core dan quad-core i7 dengan hyperthreading (2.67 GHz).

Ini angkanya:

i7

Spesifikasi: Intel i7 2x quad-core @ 2,67 GHz Tes: scala.threads.ParallelTests

Nama tes: loop_heap_read

Nomor benang .: 1 Total pengujian: 200

Waktu berjalan: (menunjukkan 5 terakhir) 9.0069 9.0036 9.0017 9.0084 9.0074 (rata-rata = 9.1034 mnt = 8.9986 maks = 21.0306)

Nomor benang .: 2 Total pengujian: 200

Waktu berjalan: (menunjukkan 5 terakhir) 4,5563 4,7128 4,5663 4,5617 4,5724 (rata-rata = 4,6337 menit = 4,5509 maks = 13,9476)

Nomor benang .: 4 Total pengujian: 200

Waktu berjalan: (menampilkan 5 terakhir) 2.3946 2.3979 2.3934 2.3937 2.3964 (rata-rata = 2.5113 menit = 2.3884 max = 13.5496)

Nomor benang .: 8 Total pengujian: 200

Waktu berjalan: (menunjukkan 5 terakhir) 2.4479 2.4362 2.4323 2.4472 2.4383 (rata-rata = 2.5562 menit = 2.4166 max = 10.3726)

Nama tes: threadlocal

Nomor benang .: 1 Total pengujian: 200

Waktu berjalan: (menampilkan 5 terakhir) 91.1741 90.8978 90.6181 90.6200 90.6113 (rata-rata = 91.0291 menit = 90.6000 maks = 129.7501)

Nomor benang .: 2 Total pengujian: 200

Waktu berjalan: (menampilkan 5 terakhir) 45,3838 45,3858 45,6676 45,3772 45,3839 (rata-rata = 46,0555 menit = 45,3726 maks = 90,7108)

Nomor benang .: 4 Total pengujian: 200

Waktu berjalan: (menunjukkan 5 terakhir) 22.8118 22.8135 59.1753 22.8229 22.8172 (rata-rata = 23.9752 menit = 22.7951 maks = 59.1753)

Nomor benang .: 8 Total pengujian: 200

Waktu berjalan: (menampilkan 5 terakhir) 22.2965 22.2415 22.3438 22.3109 22.4460 (rata-rata = 23.2676 menit = 22.2346 max = 50.3583)

AMD

Spesifikasi: AMD 8220 4x dual-core @ 2,8 GHz Test: scala.threads.ParallelTests

Nama tes: loop_heap_read

Pekerjaan total: 20000000 Jumlah benang .: 1 Tes total: 200

Waktu berjalan: (menunjukkan 5 terakhir) 12,625 12,631 12,634 12,632 12,628 (rata-rata = 12,7333 menit = 12,619 maks = 26,698)

Nama tes: loop_heap_read Total pekerjaan: 20000000

Waktu berjalan: (menampilkan 5 terakhir) 6.412 6.424 6.408 6.397 6.43 (rata-rata = 6.5367 min = 6.393 max = 19.716)

Nomor benang .: 4 Total pengujian: 200

Waktu berjalan: (menunjukkan 5 terakhir) 3.385 4.298 9.7 6.535 3.385 (rata-rata = 5.6079 min = 3.354 max = 21.603)

Nomor benang .: 8 Total pengujian: 200

Waktu berjalan: (menunjukkan 5 terakhir) 5.389 5.795 10.818 3.823 3.824 (rata-rata = 5.5810 min = 2.405 max = 19.755)

Nama tes: threadlocal

Nomor benang .: 1 Total pengujian: 200

Waktu berjalan: (menampilkan 5 terakhir) 200.217 207.335 200.241 207.342 200.23 (rata-rata = 202.2424 menit = 200.184 max = 245.369)

Nomor benang .: 2 Total pengujian: 200

Waktu berjalan: (menunjukkan 5 terakhir) 100.208 100.199 100.211 103.781 100.215 (rata-rata = 102.2238 menit = 100.192 maks = 129.505)

Nomor benang .: 4 Total pengujian: 200

Waktu berjalan: (menampilkan 5 terakhir) 62.101 67.629 62.087 52.021 55.766 (rata-rata = 65.6361 menit = 50.282 max = 167.433)

Nomor benang .: 8 Total pengujian: 200

Waktu berjalan: (menunjukkan 5 terakhir) 40.672 74.301 34.434 41.549 28.119 (rata-rata = 54.7701 menit = 28.119 max = 94.424)

Ringkasan

Lokal utas sekitar 10-20x dari heap yang dibaca. Tampaknya juga berskala baik pada implementasi JVM ini dan arsitektur ini dengan jumlah prosesor.

axel22
sumber
5
+1 Kudos karena menjadi satu-satunya yang memberikan hasil kuantitatif. Saya agak skeptis karena tes ini ada di Scala, tetapi seperti yang Anda katakan, bytecode Java seharusnya serupa ...
Gravity
Terima kasih! Ini sementara hasil loop dalam bytecode yang hampir sama dengan kode Java yang sesuai akan menghasilkan. Waktu yang berbeda dapat diamati pada VM yang berbeda, meskipun - ini telah diuji pada Sun JVM1.6.
axel22
Kode benchmark ini tidak mensimulasikan kasus penggunaan yang baik untuk ThreadLocal. Dalam metode pertama: setiap utas akan memiliki representasi bersama dalam memori, string tidak berubah. Dalam metode kedua, Anda mengukur biaya pencarian hashtable di mana string tersebut terpisah di antara semua utas.
Joelmob
String tidak berubah, tetapi dibaca dari memori (penulisan "!"tidak pernah terjadi) dalam metode pertama - metode pertama secara efektif setara dengan subclass Threaddan memberinya bidang khusus. Tolok ukur mengukur kasus tepi ekstrim di mana seluruh komputasi terdiri dari membaca variabel / utas lokal - aplikasi nyata mungkin tidak terpengaruh tergantung pada pola aksesnya, tetapi dalam kasus terburuk, mereka akan berperilaku seperti di atas.
axel22
4

Ini dia tes lain. Hasilnya menunjukkan bahwa ThreadLocal sedikit lebih lambat dari bidang biasa, tetapi dalam urutan yang sama. Aprox 12% lebih lambat

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

Keluaran:

0-Running contoh lapangan

Sampel bidang 0-End: 6044

0-Menjalankan sampel lokal thread

Sampel lokal utas 0-ujung: 6015

Sampel bidang 1-Running

Contoh bidang 1-ujung: 5095

Sampel lokal 1-Running thread

Sampel lokal utas 1-ujung: 5720

Sampel bidang 2-Running

Contoh bidang 2-ujung: 4842

2-Running thread sampel lokal

Sampel lokal utas 2 ujung: 5835

Sampel lapangan 3-Running

Sampel lapangan 3-End: 4674

3-Running thread sampel lokal

Sampel lokal utas 3-ujung: 5287

Contoh bidang 4-Running

Contoh bidang 4-ujung: 4849

4-Running thread sampel lokal

Sampel lokal utas 4-ujung: 5309

5-sampel lapangan lari

Contoh bidang 5-ujung: 4781

5-Running thread sampel lokal

Sampel lokal benang 5-ujung: 5330

6-Running contoh lapangan

Contoh bidang 6-End: 5294

6-Running thread local sample

Sampel lokal utas 6-ujung: 5511

7-Running sampel lapangan

Contoh bidang 7-End: 5119

7-Running thread sampel lokal

Sampel lokal utas 7-ujung: 5793

8-Running contoh lapangan

Contoh bidang 8-ujung: 4977

8-Running thread sampel lokal

Sampel lokal utas 8-ujung: 6374

Contoh lapangan 9-Running

Contoh bidang 9-End: 4841

9-Running thread sampel lokal

Sampel lokal utas 9-ujung: 5471

Rata-rata bidang: 5051

Rata-rata ThreadLocal: 5664

Env:

openjdk versi "1.8.0_131"

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

Ubuntu 16.04 LTS

jpereira.dll
sumber
1
Maaf, ini bahkan bukan tes yang valid. A) Masalah terbesar: Anda mengalokasikan Strings dengan setiap iterasi ( Int.toString), yang sangat mahal dibandingkan dengan apa yang Anda uji. B) Anda melakukan dua operasi peta setiap iterasi, juga sama sekali tidak terkait dan mahal. Coba tambahkan int primitif dari ThreadLocal sebagai gantinya. C) Gunakan System.nanoTimealih-alih System.currentTimeMillis, yang pertama untuk pembuatan profil, yang terakhir adalah untuk tujuan tanggal-waktu pengguna dan dapat berubah di bawah kaki Anda. D) Anda harus menghindari allocs sepenuhnya, termasuk yang tingkat atas untuk kelas "contoh" Anda
Philip Guin
3

@Pete adalah tes yang benar sebelum Anda mengoptimalkan.

Saya akan sangat terkejut jika membangun MessageDigest memiliki overhead yang serius jika dibandingkan dengan menggunakannya secara akurat.

Kehilangan penggunaan ThreadLocal dapat menjadi sumber kebocoran dan referensi yang menggantung, yang tidak memiliki siklus hidup yang jelas, umumnya saya tidak pernah menggunakan ThreadLocal tanpa rencana yang sangat jelas tentang kapan sumber daya tertentu akan dihapus.

Gareth Davis
sumber
0

Bangun dan ukurlah.

Selain itu, Anda hanya memerlukan satu threadlocal jika merangkum perilaku mencerna pesan Anda menjadi sebuah objek. Jika Anda memerlukan MessageDigest lokal dan byte lokal [1000] untuk tujuan tertentu, buat objek dengan kolom messageDigest dan byte [], lalu letakkan objek tersebut ke ThreadLocal daripada keduanya secara individual.

Pete Kirkham
sumber
Terima kasih, MessageDigest dan byte [] adalah kegunaan yang berbeda, jadi satu objek tidak diperlukan.
Sarmun