ThreadLocal変数のパフォーマンス


87

ThreadLocal通常のフィールドよりも遅い変数からの読み取りはどれくらいですか?

より具体的には、単純なオブジェクトの作成は、ThreadLocal変数へのアクセスよりも速いですか、遅いですか?

私はそれが十分に速いので、ThreadLocal<MessageDigest>インスタンスを持つことはMessageDigest毎回のインスタンスを作成するよりもはるかに速いと思います。しかし、それはたとえばバイト[10]やバイト[1000]にも当てはまりますか?

編集:質問は、ThreadLocal's getを呼び出すときに実際に何が起こっているのですか?それが他の分野と同じように単なる分野である場合、答えは「常に最速」ですよね?


2
ローカルスレッドは、基本的に、ハッシュマップとルックアップを含むフィールドであり、キーは現在のスレッドオブジェクトです。したがって、はるかに低速ですが、それでも高速です。:)
eckes 2015年

1
@eckes:確かにそのように動作しますが、通常はこのように実装されていません。代わりに、Threadsには、キーが現在のThreadLocalオブジェクトである(同期されていない)ハッシュマップが含まれています
sbk 2016年

回答:


40

未公開のベンチマークをThreadLocal.get実行すると、私のマシンでは反復ごとに約35サイクルかかります。大したことではありません。Sunの実装では、カスタム線形プロービングハッシュマップがsを値にThreadマップThreadLocalします。単一のスレッドによってのみアクセスされるため、非常に高速になる可能性があります。

小さなオブジェクトの割り当てには同様のサイクル数がかかりますが、キャッシュが枯渇するため、タイトなループでは数値がやや低くなる可能性があります。

の建設はMessageDigest比較的高価になる可能性があります。かなりの量の状態があり、構築はProviderSPIメカニズムを介して行われます。たとえば、のクローンを作成したり、を提供したりすることで、最適化できる場合がありますProvider

ThreadLocal作成するよりもキャッシュする方が速い場合があるからといって、必ずしもシステムパフォーマンスが向上するわけではありません。GCに関連する追加のオーバーヘッドが発生し、すべてが遅くなります。

アプリケーションが非常に頻繁に使用するMessageDigest場合を除いて、代わりに従来のスレッドセーフキャッシュの使用を検討することをお勧めします。


5
IMHO、最速の方法は、SPIを無視して、のようなものを使用することですnew org.bouncycastle.crypto.digests.SHA1Digest()。キャッシュに勝るものはないと確信しています。
maaartinus 2011年

57

2009年には、一部のJVMは、オブジェクトでThreadLocal非同期を使用して実装さHashMapれましたThread.currentThread()。これにより、非常に高速になり(もちろん、通常のフィールドアクセスを使用する場合ほど高速ではありません)、死んだThreadLocalときにオブジェクトが整理されるようになりましThreadた。2016年にこの回答を更新すると、ほとんどの(すべて?)新しいJVMはThreadLocalMap線形プロービングでを使用しているようです。それらのパフォーマンスについてはよくわかりませんが、以前の実装よりも大幅に悪いとは想像できません。

もちろん、new Object()最近は非常に高速であり、ガベージコレクターは短命のオブジェクトの再生にも非常に優れています。

オブジェクトの作成に費用がかかることが確実な場合、またはスレッドごとに状態を維持する必要がある場合を除いて、必要なときに単純な割り当てを行いThreadLocal、プロファイラーの場合にのみ実装に切り替えることをお勧めします。あなたがする必要があることをあなたに伝えます。


4
実際に質問に答える唯一の答えであるための+1。
cletus 2009年

ThreadLocalMapに線形プロービングを使用しない最新のJVMの例を教えてください。Java 8 OpenJDKは、まだ線形プロービングでThreadLocalMapを使用しているようです。grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/…– Karthick 2016
20:23

1
@Karthick申し訳ありませんが、できません。私はこれを2009年に書き戻しました。更新します。
ビルミシェル2016年

34

良い質問です、私は最近それを自問しています。明確な数値を提供するために、以下のベンチマーク(Scalaでは、同等の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)
}

ここで入手できるのは、AMD 4x 2.8 GHzデュアルコアとハイパースレッディング(2.67 GHz)を備えたクアッドコアi7で実行されました。

これらは数字です:

i7

仕様:Intel i72xクアッドコア@ 2.67 GHzテスト:scala.threads.ParallelTests

テスト名:loop_heap_read

スレッド番号:1合計テスト:200

実行時間:(最後の5つを表示)9.069 9.0036 9.0017 9.0084 9.0074(平均= 9.1034分= 8.9986最大= 21.0306)

スレッド番号:2合計テスト:200

実行時間:(最後の5つを表示)4.5563 4.7128 4.5663 4.5617 4.5724(平均= 4.6337最小= 4.5509最大= 13.9476)

スレッド番号:4合計テスト:200

実行時間:(最後の5つを表示)2.3946 2.3979 2.3934 2.3937 2.3964(平均= 2.5113最小= 2.3884最大= 13.5496)

スレッド番号:8合計テスト:200

実行時間:(最後の5つを表示)2.4479 2.4362 2.4323 2.4472 2.4383(平均= 2.5562最小= 2.4166最大= 10.3726)

テスト名:threadlocal

スレッド番号:1合計テスト:200

実行時間:(最後の5つを表示)91.1741 90.8978 90.6181 90.6200 90.6113(平均= 91.0291最小= 90.6000最大= 129.7501)

スレッド番号:2合計テスト:200

実行時間:(最後の5つを表示)45.3838 45.3858 45.6676 45.3772 45.3839(平均= 46.0555最小= 45.3726最大= 90.7108)

スレッド番号:4合計テスト:200

実行時間:(最後の5つを表示)22.8118 22.8135 59.1753 22.8229 22.8172(平均= 23.9752最小= 22.7951最大= 59.1753)

スレッド番号:8合計テスト:200

実行時間:(最後の5つを表示)22.2965 22.2415 22.3438 22.3109 22.4460(平均= 23.2676最小= 22.2346最大= 50.3583)

AMD

仕様:AMD 82204xデュアルコア@ 2.8 GHzテスト:scala.threads.ParallelTests

テスト名:loop_heap_read

総作業量:20000000スレッド数:1総テスト数:200

実行時間:(最後の5つを表示)12.625 12.631 12.634 12.632 12.628(平均= 12.7333最小= 12.619最大= 26.698)

テスト名:loop_heap_read総作業量:20000000

実行時間:(最後の5つを表示)6.412 6.424 6.408 6.397 6.43(平均= 6.5367最小= 6.393最大= 19.716)

スレッド番号:4合計テスト:200

実行時間:(最後の5つを表示)3.385 4.298 9.7 6.535 3.385(平均= 5.6079最小= 3.354最大= 21.603)

スレッド番号:8合計テスト:200

実行時間:(最後の5つを表示)5.389 5.795 10.818 3.823 3.824(平均= 5.5810最小= 2.405最大= 19.755)

テスト名:threadlocal

スレッド番号:1合計テスト:200

実行時間:(最後の5つを表示)200.217 207.335 200.241 207.342 200.23(平均= 202.2424最小= 200.184最大= 245.369)

スレッド番号:2合計テスト:200

実行時間:(最後の5つを表示)100.208 100.199 100.211 103.781 100.215(平均= 102.2238最小= 100.192最大= 129.505)

スレッド番号:4合計テスト:200

実行時間:(最後の5つを表示)62.101 67.629 62.087 52.021 55.766(平均= 65.6361最小= 50.282最大= 167.433)

スレッド番号:8合計テスト:200

実行時間:(最後の5つを表示)40.672 74.301 34.434 41.549 28.119(平均= 54.7701最小= 28.119最大= 94.424)

概要

ローカルスレッドは、読み取られたヒープの約10〜20倍です。また、このJVM実装と、プロセッサの数に応じたこれらのアーキテクチャでも適切に拡張できるようです。


5
定量的な結果を出す唯一の人であることに対する+1の称賛。これらのテストはScalaで行われているため、私は少し懐疑的ですが、あなたが言ったように、Javaバイトコードは似ているはずです...
Gravity

ありがとう!このwhileループは、対応するJavaコードが生成するのと実質的に同じバイトコードになります。ただし、VMごとに異なる時間が観察される可能性があります。これはSunJVM1.6でテストされています。
axel22 2011

このベンチマークコードは、ThreadLocalの適切なユースケースをシミュレートしていません。最初の方法では、すべてのスレッドがメモリ内に共有表現を持ち、文字列は変更されません。2番目の方法では、文字列がすべてのスレッド間で論理和であるハッシュテーブルルックアップのコストをベンチマークします。
Joelmob 2017年

文字列は変更されませんが"!"、最初のメソッドでメモリから読み取られます(書き込みは発生しません)。最初のメソッドは、サブクラスThread化してカスタムフィールドを指定することと実質的に同等です。ベンチマークは、計算全体が変数/スレッドローカルの読み取りで構成される極端なエッジケースを測定します。実際のアプリケーションは、アクセスパターンによっては影響を受けない場合がありますが、最悪の場合、上記のように動作します。
axel22 2017年

4

ここで別のテストを行います。結果は、ThreadLocalが通常のフィールドよりも少し遅いが、同じ順序であることを示しています。Aprox 12%遅い

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

出力:

0-実行中のフィールドサンプル

0-終了フィールドサンプル:6044

0-実行中のスレッドローカルサンプル

0-スレッドローカルサンプルの終了:6015

1-ランニングフィールドサンプル

1-エンドフィールドサンプル:5095

1-実行中のスレッドローカルサンプル

1-エンドスレッドローカルサンプル:5720

2-ランニングフィールドサンプル

2-エンドフィールドサンプル:4842

2-実行中のスレッドローカルサンプル

2-エンドスレッドローカルサンプル:5835

3-ランニングフィールドサンプル

3-エンドフィールドサンプル:4674

3-実行中のスレッドローカルサンプル

3-エンドスレッドローカルサンプル:5287

4-ランニングフィールドサンプル

4-エンドフィールドサンプル:4849

4-実行中のスレッドローカルサンプル

4-エンドスレッドローカルサンプル:5309

5-ランニングフィールドサンプル

5-エンドフィールドサンプル:4781

5-実行中のスレッドローカルサンプル

5-エンドスレッドローカルサンプル:5330

6-ランニングフィールドサンプル

6-エンドフィールドサンプル:5294

6-実行中のスレッドローカルサンプル

6-スレッドローカルサンプルの終了:5511

7-ランニングフィールドサンプル

7-エンドフィールドサンプル:5119

7-実行中のスレッドローカルサンプル

7-エンドスレッドローカルサンプル:5793

8-ランニングフィールドサンプル

8-エンドフィールドサンプル:4977

8-実行中のスレッドローカルサンプル

8-エンドスレッドローカルサンプル:6374

9-ランニングフィールドサンプル

9-エンドフィールドサンプル:4841

9-実行中のスレッドローカルサンプル

9-スレッドローカルサンプルの終了:5471

フィールド平均:5051

ThreadLocal avg:5664

環境:

openjdkバージョン「1.8.0_131」

インテル®Core™i7-7500UCPU @ 2.70GHz×4

Ubuntu 16.04 LTS


1
申し訳ありませんが、これは有効なテストに近いものではありません。A)最大の問題:反復ごとに文字列を割り当てています(Int.toString)これは、テストしているものと比較して非常にコストがかかります。B)反復ごとに2つのマップ操作を実行しており、これもまったく無関係で高価です。代わりに、ThreadLocalからプリミティブintをインクリメントしてみてください。C)のSystem.nanoTime代わりに使用します。System.currentTimeMillis前者はプロファイリング用、後者はユーザーの日時用であり、足元で変更できます。D)あなたの「例」のクラスのトップレベルのものも含めて、完全にallocsを避ける必要があります
フィリップ・グイン

3

@Peteは、最適化する前の正しいテストです。

MessageDigestを実際に使用する場合と比較して、MessageDigestの構築に深刻なオーバーヘッドがあるとしたら、私は非常に驚きます。

ThreadLocalの使用ミスは、明確なライフサイクルを持たないリークやダングリング参照の原因となる可能性があります。通常、特定のリソースがいつ削除されるかについての明確な計画がなければ、ThreadLocalを使用することはありません。


0

それを構築し、測定します。

また、メッセージダイジェスト動作をオブジェクトにカプセル化する場合、必要なスレッドローカルは1つだけです。何らかの目的でローカルMessageDigestとローカルbyte [1000]が必要な場合は、messageDigestフィールドとbyte []フィールドを使用してオブジェクトを作成し、そのオブジェクトを個別にではなくThreadLocalに配置します。


おかげで、MessageDigestとbyte []は異なる用途であるため、1つのオブジェクトは必要ありません。
sarmun 2009年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.