ThreadLocal 변수의 성능


86

ThreadLocal일반 필드보다 느린 변수 에서 얼마나 많이 읽 습니까?

더 구체적으로 간단한 객체 생성이 ThreadLocal변수에 대한 액세스보다 빠르거나 느린 가요?

ThreadLocal<MessageDigest>인스턴스 를 갖는 것이 MessageDigest매번 인스턴스를 만드는 것보다 훨씬 빠를 정도로 충분히 빠르다고 가정합니다 . 하지만 예를 들어 byte [10] 또는 byte [1000]에도 적용됩니까?

편집 : 질문은 ThreadLocal's get을 호출 할 때 실제로 무슨 일이 일어나고 있습니까? 그게 다른 분야와 마찬가지로 그저 필드라면 대답은 "항상 가장 빠르다"입니다. 맞죠?


2
스레드 로컬은 기본적으로 해시 맵과 키가 현재 스레드 객체 인 조회를 포함하는 필드입니다. 따라서 훨씬 느리지 만 여전히 빠릅니다. :)
eckes

1
@eckes : 확실히 그렇게 동작하지만 일반적으로 이런 방식으로 구현되지는 않습니다. 대신, Threads는 키가 현재 ThreadLocal객체 인 (동기화되지 않은) 해시 맵을 포함 합니다
sbk

답변:


40

게시되지 않은 벤치 마크를 실행 ThreadLocal.get하면 내 컴퓨터에서 반복 당 약 35주기가 걸립니다. 그다지 중요하지 않습니다. Sun의 구현에서 사용자 정의 선형 프로빙 해시 맵은 s를 값 으로 Thread매핑 ThreadLocal합니다. 단일 스레드에서만 액세스 할 수 있기 때문에 매우 빠를 수 있습니다.

작은 개체의 할당은 비슷한 수의주기를 필요로하지만 캐시 고갈로 인해 타이트한 루프에서 다소 낮은 수치를 얻을 수 있습니다.

의 건설은 MessageDigest상대적으로 비용이 많이 듭니다. 그것은 상당한 양의 상태를 가지고 있으며 ProviderSPI 메커니즘을 통과합니다 . 예를 들어 .NET Framework를 복제하거나 제공하여 최적화 할 수 있습니다 Provider.

ThreadLocal생성 하는 것보다 캐시하는 것이 더 빠르다고해서 반드시 시스템 성능이 향상되는 것은 아닙니다. 모든 것을 느리게하는 GC와 관련된 추가 오버 헤드가 있습니다.

응용 프로그램이 매우 많이 사용하지 않는 MessageDigest한 기존의 스레드 안전 캐시를 대신 사용하는 것이 좋습니다.


5
IMHO, 가장 빠른 방법은 SPI를 무시하고 new org.bouncycastle.crypto.digests.SHA1Digest(). 나는 어떤 캐시도 그것을 이길 수 없다고 확신합니다.
maaartinus 2011 년

57

2009 년에 일부 JVM은 Thread.currentThread () 객체에서 동기화되지 않은 HashMap을 사용하여 ThreadLocal을 구현했습니다. 이로 인해 (물론 일반 필드 액세스를 사용하는 것만 큼 빠르지는 않지만) 매우 빠르며 ThreadLocal 객체가 Thread가 죽었을 때 정리되었습니다. 2016 년 에이 답변을 업데이트하면 대부분의 (모두?) 최신 JVM이 선형 프로브와 함께 ThreadLocalMap을 사용하는 것 같습니다. 나는 그것들의 성능에 대해 확신하지 못합니다. 그러나 이것이 이전 구현보다 훨씬 더 나쁘다는 것을 상상할 수 없습니다.

물론 최근에는 new Object ()도 매우 빠르며 가비지 콜렉터도 수명이 짧은 객체를 회수하는 데 매우 능숙합니다.

객체 생성에 비용이 많이들 것이라는 확신이 없거나 스레드 단위로 스레드에서 일부 상태를 유지해야하는 경우가 아니면 필요한 경우 더 간단한 할당 솔루션을 사용하고 다음과 같은 경우에만 ThreadLocal 구현으로 전환하는 것이 좋습니다. 프로파일 러가 필요하다고 알려줍니다.


4
+1은 실제로 질문에 대한 유일한 답변입니다.
cletus

ThreadLocalMap에 대해 선형 프로브를 사용하지 않는 최신 JVM의 예를 들어 줄 수 있습니까? Java 8 OpenJDK는 여전히 선형 프로브와 함께 ThreadLocalMap을 사용하는 것 같습니다. grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/…
Karthick

1
@Karthick 죄송합니다. 나는 이것을 2009 년에 다시 썼다. 나는 갱신 할 것이다.
Bill Michell 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.8GHz 듀얼 코어 및 하이퍼 스레딩 (2.67GHz)이있는 쿼드 코어 i7에서 수행되었습니다.

다음은 숫자입니다.

i7

사양 : Intel i7 2x 쿼드 코어 @ 2.67GHz 테스트 : 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.8GHz 테스트 : 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마다 다른 시간이 관찰 될 수 있습니다. 이것은 Sun JVM1.6에서 테스트되었습니다.
axel22

이 벤치 마크 코드는 ThreadLocal에 대한 좋은 사용 사례를 시뮬레이션하지 않습니다. 첫 번째 방법에서 : 모든 스레드는 메모리에서 공유 된 표현을 가지며 문자열은 변경되지 않습니다. 두 번째 방법에서는 문자열이 모든 스레드간에 분리되어있는 해시 테이블 조회 비용을 벤치마킹합니다.
Joelmob

문자열은 변경되지 않지만 "!"첫 번째 메서드 에서 메모리에서 읽습니다 (쓰기가 발생하지 않음). 첫 번째 메서드는 서브 클래 싱 Thread과 사용자 지정 필드를 제공하는 것과 사실상 동일 합니다. 벤치 마크는 전체 계산이 변수 / 스레드 로컬 읽기로 구성된 극단적 인 경우를 측정합니다. 실제 응용 프로그램은 액세스 패턴에 따라 영향을받지 않을 수 있지만 최악의 경우 위와 같이 동작합니다.
axel22

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-End 필드 샘플 : 6044

0- 실행 스레드 로컬 샘플

0-End 스레드 로컬 샘플 : 6015

1- 러닝 필드 샘플

1-End 필드 샘플 : 5095

1- 실행 스레드 로컬 샘플

1- 엔드 스레드 로컬 샘플 : 5720

2- 러닝 필드 샘플

2-End 필드 샘플 : 4842

2- 실행 스레드 로컬 샘플

2- 엔드 스레드 로컬 샘플 : 5835

3- 러닝 필드 샘플

3-End 필드 샘플 : 4674

3- 실행 스레드 로컬 샘플

3- 엔드 스레드 로컬 샘플 : 5287

4- 러닝 필드 샘플

4- 엔드 필드 샘플 : 4849

4- 실행 스레드 로컬 샘플

4- 엔드 스레드 로컬 샘플 : 5309

5- 러닝 필드 샘플

5-End 필드 샘플 : 4781

5- 실행 스레드 로컬 샘플

5- 엔드 스레드 로컬 샘플 : 5330

6- 러닝 필드 샘플

6-End 필드 샘플 : 5294

6- 실행 스레드 로컬 샘플

6- 엔드 스레드 로컬 샘플 : 5511

7- 실행 필드 샘플

7-End 필드 샘플 : 5119

7- 실행 스레드 로컬 샘플

7-End 스레드 로컬 샘플 : 5793

8- 실행 필드 샘플

8-End 필드 샘플 : 4977

8- 실행 스레드 로컬 샘플

8- 엔드 스레드 로컬 샘플 : 6374

9- 러닝 필드 샘플

9-End 필드 샘플 : 4841

9- 실행 스레드 로컬 샘플

9- 엔드 스레드 로컬 샘플 : 5471

필드 평균 : 5051

ThreadLocal 평균 : 5664

환경 :

openjdk 버전 "1.8.0_131"

Intel® Core ™ i7-7500U CPU @ 2.70GHz × 4

Ubuntu 16.04 LTS


죄송합니다. 이것은 유효한 테스트에 가깝지 않습니다. A) 가장 큰 문제 : 반복 할 때마다 문자열을 할당합니다 ( Int.toString), 테스트하는 것에 비해 매우 비쌉니다. B) 반복 할 때마다 두 번의 맵 작업을 수행하고, 또한 완전히 관련이없고 비용이 많이 듭니다. 대신 ThreadLocal에서 기본 int를 증가 시키십시오. C) System.nanoTime대신 사용 System.currentTimeMillis, 전자는 프로파일 링용이고 후자는 사용자 날짜-시간 용이며 발 아래에서 변경할 수 있습니다. D) "예제"클래스의 최상위 레벨을 포함하여 할당을 완전히 피해야합니다
Philip Guin

3

@Pete는 최적화하기 전에 올바른 테스트입니다.

MessageDigest를 구성 할 때 실제로 사용하는 것과 비교할 때 심각한 오버 헤드가 있으면 매우 놀랍습니다.

ThreadLocal을 사용하지 않는 것은 명확한 수명주기가없는 누수 및 매달려있는 참조의 원인이 될 수 있습니다. 일반적으로 특정 리소스가 제거 될시기에 대한 명확한 계획 없이는 ThreadLocal을 사용하지 않습니다.


0

그것을 구축하고 측정하십시오.

또한 메시지 요약 동작을 객체로 캡슐화하는 경우 하나의 threadlocal 만 필요합니다. 어떤 목적으로 로컬 MessageDigest와 로컬 byte [1000]가 필요한 경우 messageDigest 및 byte [] 필드가있는 개체를 만들고 해당 개체를 개별적으로 두지 않고 ThreadLocal에 넣습니다.


감사합니다. MessageDigest와 byte []는 서로 다른 용도이므로 하나의 객체가 필요하지 않습니다.
Sarmun
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.