ThreadLocal変数のパフォーマンス
-
03-07-2019 - |
質問
どこから読まれるか ThreadLocal
変数は通常のフィールドからの変数よりも遅いですか?
より具体的には、単純なオブジェクトの作成がアクセスよりも速いか遅いかです。 ThreadLocal
変数?
十分に速いと思いますので、 ThreadLocal<MessageDigest>
インスタンスは、のインスタンスを作成するよりもはるかに高速です MessageDigest
毎回。しかし、それはたとえば byte[10] や byte[1000] にも当てはまりますか?
編集:問題は、電話をかけるときに実際に何が起こっているのかということです ThreadLocal
もらえるの?それが他のフィールドと同様に単なるフィールドであれば、答えは「常に最速です」になりますよね?
解決
未公開のベンチマークを実行すると、ThreadLocal.get
はマシン上で反復ごとに約35サイクルかかります。大したことではない。 Sunの実装では、Thread
のカスタム線形プローブハッシュマップはThreadLocal
sを値にマッピングします。単一のスレッドによってのみアクセスされるため、非常に高速です。
小さなオブジェクトの割り当てには、同様のサイクル数がかかりますが、キャッシュが枯渇するため、タイトなループでは多少低い数値になる場合があります。
MessageDigest
の構築は、比較的高価になる可能性があります。かなりの量の状態があり、構造はProvider
SPIメカニズムを通過します。たとえば、<=>を複製または提供することで最適化できる場合があります。
作成するよりも<=>でキャッシュする方が速い場合があるため、必ずしもシステムのパフォーマンスが向上するわけではありません。 GCに関連する追加のオーバーヘッドが発生し、すべてが遅くなります。
アプリケーションで<=>を非常に頻繁に使用しない限り、代わりに従来のスレッドセーフキャッシュの使用を検討することをお勧めします。
他のヒント
2009年、一部のJVMはThread.currentThread()オブジェクトの非同期HashMapを使用してThreadLocalを実装しました。これにより、非常に高速になりました(もちろん、通常のフィールドアクセスを使用する場合ほど高速ではありませんが)。また、スレッドが死んだときにThreadLocalオブジェクトが整頓されます。 2016年にこの回答を更新すると、ほとんど(すべて?)の新しいJVMが線形探査でThreadLocalMapを使用しているようです。これらの<!>#8211のパフォーマンスについては不確かです。しかし、以前の実装よりも著しく悪いとは想像できません。
もちろん、最近では新しいObject()も非常に高速であり、ガベージコレクターは短命のオブジェクトの回収にも非常に優れています。
オブジェクトの作成にコストがかかることを確信していない場合、またはスレッドごとに状態を永続化する必要がある場合を除き、必要な場合はより単純な割り当てを行い、ThreadLocalに切り替えることをお勧めしますプロファイラーから必要があることが通知された場合の実装。
良い質問です、私は最近そのことを自問してきました。明確な数値を得るために、以下のベンチマーク(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 i7 2xクアッドコア@ 2.67 GHz テスト:scala.threads.ParallelTests
テスト名:loop_heap_read
スレッド番号:1 合計テスト:200
実行時間:(最後の5つを表示) 9.0069 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 8220 4xデュアルコア@ 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実装とこれらのアーキテクチャで、プロセッサの数に応じて適切に拡張できるようです。
別のテストに進みます。結果は、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-Endフィールドのサンプル:6044
0-実行中のスレッドローカルサンプル
0-エンドスレッドローカルサンプル:6015
1-ランニングフィールドのサンプル
1-Endフィールドのサンプル:5095
1-実行中のスレッドローカルサンプル
1-end thread local sample:5720
2-ランニングフィールドサンプル
2-Endフィールドのサンプル:4842
2-実行中のスレッドローカルサンプル
2エンドスレッドローカルサンプル:5835
3-ランニングフィールドサンプル
3-Endフィールドのサンプル:4674
3-実行中のスレッドローカルサンプル
3エンドスレッドローカルサンプル:5287
4-ランニングフィールドサンプル
4-Endフィールドサンプル:4849
4-実行中のスレッドローカルサンプル
4-Endスレッドローカルサンプル:5309
5-ランニングフィールドサンプル
5-Endフィールドのサンプル:4781
5-Runningスレッドローカルサンプル
5-end thread local sample:5330
6-ランニングフィールドサンプル
6-Endフィールドのサンプル:5294
6-実行中のスレッドローカルサンプル
6-end thread local sample:5511
7-ランニングフィールドサンプル
7-Endフィールドのサンプル:5119
7-実行中のスレッドローカルサンプル
7エンドスレッドローカルサンプル:5793
8-ランニングフィールドのサンプル
8-Endフィールドサンプル:4977
8-実行中のスレッドローカルサンプル
8エンドスレッドローカルサンプル:6374
9-ランニングフィールドのサンプル
9終了フィールドのサンプル:4841
9-実行中のスレッドローカルサンプル
9エンドスレッドローカルサンプル:5471
フィールド平均:5051
ThreadLocal avg:5664
環境:
openjdkバージョン<!> quot; 1.8.0_131 <!> quot;
Intel <!>#174;コア<!>#8482; i7-7500U CPU @ 2.70GHz <!>#215; 4
Ubuntu 16.04 LTS
@Peteは最適化する前の正しいテストです。
MessageDigest を実際に使用する場合と比較して、MessageDigest の構築に重大なオーバーヘッドがあるとしたら、私は非常に驚くでしょう。
ThreadLocal を使用しないと、明確なライフサイクルがないリークや未解決参照の原因となる可能性があります。通常、私は特定のリソースがいつ削除されるかについて明確な計画がなければ ThreadLocal を使用しません。
ビルドして測定します。
また、メッセージダイジェストの動作をオブジェクトにカプセル化する場合、必要なスレッドローカルは1つだけです。何らかの目的でローカルのMessageDigestとローカルのbyte [1000]が必要な場合は、messageDigestとbyte []フィールドを持つオブジェクトを作成し、そのオブジェクトを両方ではなくThreadLocalに入れます。