문제

저는 스레드 로컬 영역에서 할당하여 작동하는 D 프로그래밍 언어용 사용자 지정 마크 릴리스 스타일 메모리 할당자를 작업 중입니다.할당당 하나의 TLS 조회만 갖도록 코드를 설계한 후에도 스레드 로컬 저장소 병목 현상으로 인해 동일한 단일 스레드 버전의 코드에 비해 이 영역에서 메모리를 할당하는 데 엄청난(~50%) 속도가 느려지는 것 같습니다. 할당 해제.이는 루프에서 여러 번 메모리를 할당/해제하는 것을 기반으로 하며 이것이 내 벤치마킹 방법의 아티팩트인지 알아내려고 노력하고 있습니다.제가 이해한 바에 따르면 스레드 로컬 저장소는 기본적으로 포인터를 통해 변수에 액세스하는 것과 유사하게 추가 간접 계층을 통해 무언가에 액세스해야 합니다.이것이 잘못된 것입니까?스레드 로컬 저장소에는 일반적으로 어느 정도의 오버헤드가 있습니까?

메모:D에 대해 언급했지만 D에 국한되지 않는 일반적인 답변에도 관심이 있습니다. D의 스레드 로컬 저장소 구현이 최상의 구현보다 느리면 향상될 가능성이 높기 때문입니다.

도움이 되었습니까?

해결책

속도는 TLS 구현에 따라 다릅니다.

예, TLS가 포인터 조회만큼 빠를 수 있습니다. 메모리 관리 장치가있는 시스템에서도 더 빠를 수도 있습니다.

포인터 조회의 경우 스케줄러의 도움이 필요합니다. 스케줄러는 작업 스위치에서 TLS 데이터에 대한 포인터를 업데이트해야합니다.

TLS를 구현하는 또 다른 빠른 방법은 메모리 관리 장치를 통한 것입니다. 여기서 TLS는 TLS 변수가 특수 세그먼트에 할당된다는 점을 제외하고 다른 데이터와 마찬가지로 처리됩니다. 스케줄러는 작업 스위치에서 올바른 메모리 청크를 작업의 주소 공간에 매핑합니다.

스케줄러가 이러한 방법을 지원하지 않으면 컴파일러/라이브러리가 다음을 수행해야합니다.

  • 현재 ThreadId를 얻으십시오
  • 세마포어를 가져 가십시오
  • ThreadID에 의해 TLS 블록에 대한 포인터 조회 (지도를 사용할 수 있음)
  • 세마포어를 해제하십시오
  • 그 포인터를 반환하십시오.

분명히 각 TLS 데이터 액세스에 대해이 모든 작업을 수행하려면 시간이 걸리며 최대 세 번의 OS 호출이 필요할 수 있습니다. 스레드를 얻고 세마포어를 가져 가서 해제합니다.

세마포어는 TLS 포인터 목록에서 스레드를 읽지 않도록 해야하는 반면 다른 스레드는 새 스레드를 산란하는 중간에있는 BTW입니다. (따라서 새로운 TLS 블록을 할당하고 데이터를 수정).

불행히도 실제로 느린 TLS 구현을 보는 것은 드문 일이 아닙니다.

다른 팁

D의 스레드 현지인은 정말 빠릅니다. 내 테스트는 다음과 같습니다.

64 비트 우분투, 코어 i5, DMD v2.052 컴파일러 옵션 : DMD -O- 릴리스 -inline -m64

// this loop takes 0m0.630s
void main(){
    int a; // register allocated
    for( int i=1000*1000*1000; i>0; i-- ){
        a+=9;
    }
}

// this loop takes 0m1.875s
int a; // thread local in D, not static
void main(){
    for( int i=1000*1000*1000; i>0; i-- ){
        a+=9;
    }
}

따라서 1000*1000*1000 스레드 로컬 액세스 당 CPU 코어 중 하나 중 1.2 초만 손실됩니다. 스레드 로컬은 %FS 레지스터를 사용하여 액세스하므로 몇 가지 프로세서 명령 만 있습니다.

objdump -d로 분해 :

- this is local variable in %ecx register (loop counter in %eax):
   8:   31 c9                   xor    %ecx,%ecx
   a:   b8 00 ca 9a 3b          mov    $0x3b9aca00,%eax
   f:   83 c1 09                add    $0x9,%ecx
  12:   ff c8                   dec    %eax
  14:   85 c0                   test   %eax,%eax
  16:   75 f7                   jne    f <_Dmain+0xf>

- this is thread local, %fs register is used for indirection, %edx is loop counter:
   6:   ba 00 ca 9a 3b          mov    $0x3b9aca00,%edx
   b:   64 48 8b 04 25 00 00    mov    %fs:0x0,%rax
  12:   00 00 
  14:   48 8b 0d 00 00 00 00    mov    0x0(%rip),%rcx        # 1b <_Dmain+0x1b>
  1b:   83 04 08 09             addl   $0x9,(%rax,%rcx,1)
  1f:   ff ca                   dec    %edx
  21:   85 d2                   test   %edx,%edx
  23:   75 e6                   jne    b <_Dmain+0xb>

컴파일러는 레지스터로 루프하기 전에 더 영리하고 캐시 스레드 로컬 일 수 있으며 마지막에 로컬로 돌아가는 스레드로 반환 할 수 있지만 (GDC 컴파일러와 비교하는 것은 흥미 롭습니다), 지금도 문제는 매우 좋은 IMHO입니다.

벤치 마크 결과를 해석하는 데 매우주의해야합니다. 예를 들어, D 뉴스 그룹의 최근 스레드는 DMD의 코드 생성이 산술을 한 루프에서 큰 둔화를 일으킨다는 벤치 마크에서 결론을 내 렸지만 실제로 소비 된 시간은 긴 분할을 한 런타임 도우미 기능에 의해 지배적이었습니다. 컴파일러의 코드 생성은 둔화와 관련이 없었습니다.

TLS에 대해 어떤 종류의 코드가 생성되는지 확인하려면 Compile 및 Obj2asm이 코드를 확인하십시오.

__thread int x;
int foo() { return x; }

TLS는 Linux와는 Windows에서 매우 다르게 구현되며 OSX에서 다시 매우 다릅니다. 그러나 모든 경우에는 정적 메모리 위치의 간단한 부하보다 더 많은 지침이 될 것입니다. TLS는 항상 간단한 액세스에 비해 느리게 진행됩니다. 단단한 루프로 TLS 글로벌에 액세스하는 것도 느리게 진행됩니다. 대신 TLS 값을 캐싱하십시오.

몇 년 전에 스레드 풀 할당 코드를 작성했으며 TLS 핸들을 수영장에 캐시했습니다.

컴파일러 TLS 지원을 사용할 수 없는 경우 TLS를 직접 관리할 수 있습니다.C++용 래퍼 템플릿을 만들었으므로 기본 구현을 쉽게 교체할 수 있습니다.이 예에서는 Win32용으로 구현했습니다.메모:프로세스 당 무제한 수의 TLS 지수를 얻을 수 없으므로 (적어도 Win32에 따라) 모든 스레드 특정 데이터를 보유 할 수있을만큼 큰 힙 블록을 가리켜 야합니다.이렇게 하면 최소한의 TLS 색인 및 관련 쿼리를 얻을 수 있습니다."최상의 경우"에서는 스레드당 하나의 개인 힙 블록을 가리키는 TLS 포인터가 1개만 있습니다.

간단히 말해서:단일 개체를 가리키지 말고 대신 개체 포인터를 보유하는 스레드별 힙 메모리/컨테이너를 가리켜 더 나은 성능을 얻으세요.

다시 사용하지 않을 경우 메모리를 확보하는 것을 잊지 마십시오.저는 스레드를 클래스(Java와 마찬가지로)로 래핑하고 생성자와 소멸자로 TLS를 처리함으로써 이를 수행합니다.또한 스레드 핸들 및 ID와 같이 자주 사용되는 데이터를 클래스 멤버로 저장합니다.

용법:

유형*:tl_ptr<유형>

const 유형*의 경우:tl_ptr<상수 유형>

유형* const의 경우:const tl_ptr<유형>

const 유형* const:const tl_ptr<const 유형>

template<typename T>
class tl_ptr {
protected:
    DWORD index;
public:
    tl_ptr(void) : index(TlsAlloc()){
        assert(index != TLS_OUT_OF_INDEXES);
        set(NULL);
    }
    void set(T* ptr){
        TlsSetValue(index,(LPVOID) ptr);
    }
    T* get(void)const {
        return (T*) TlsGetValue(index);
    }
    tl_ptr& operator=(T* ptr){
        set(ptr);
        return *this;
    }
    tl_ptr& operator=(const tl_ptr& other){
        set(other.get());
        return *this;
    }
    T& operator*(void)const{
        return *get();
    }
    T* operator->(void)const{
        return get();
    }
    ~tl_ptr(){
        TlsFree(index);
    }
};

임베디드 시스템을위한 멀티 태스킹 커를 설계했으며 개념적으로 스레드-로컬 스토리지의 핵심 요구 사항은 컨텍스트 스위치 메소드가 CPU 레지스터와 함께 스레드-로컬 스토리지에 대한 포인터를 저장/복원하는 것입니다. 시작되면 항상 동일한 코드 세트를 실행하는 임베디드 시스템의 경우 하나의 포인터를 단순히 저장/복원하는 것이 가장 쉽습니다.이 포인터는 각 스레드의 고정식 블록을 가리 킵니다. 멋지고 깨끗하고 쉽고 효율적입니다.

이러한 접근 방식은 모든 스레드 내에 할당 된 모든 스레드-로컬 변수 (실제로 사용하지 않는 것)에 할당 된 모든 스레드-로컬 변수에 대한 공간을 신경 쓰지 않는다면 잘 작동합니다. 단일 구조물로 정의됩니다. 이 시나리오에서 스레드-로컬 변수에 대한 액세스는 다른 변수에 대한 액세스만큼 빨리 빠질 수 있으며, 유일한 차이점은 추가 포인터 단축입니다. 불행히도 많은 PC 응용 프로그램에는 더 복잡한 것이 필요합니다.

PC의 일부 프레임 워크에서 스레드에는 해당 변수를 사용하는 모듈이 해당 스레드에서 실행 된 경우 스레드에만 공간이 할당됩니다. 이것은 때때로 유리할 수 있지만, 다른 스레드가 종종 로컬 스토리지를 다르게 배치 할 것임을 의미합니다. 결과적으로 스레드는 변수가 위치한 위치에 대한 일종의 검색 가능한 색인을 갖고 해당 인덱스를 통해 해당 변수에 대한 모든 액세스를 지시해야 할 수도 있습니다.

프레임 워크가 소량의 고정 형식 저장소를 할당하면 많은 시나리오에서도 단일 항목 캐시조차도 꽤 높은 적중률.

TLS (Windows에서)의 유사한 성능 문제를 보았습니다. 우리는 제품의 "커널"내부의 특정 중요한 작업에 의존합니다. 약간의 노력 후에 나는 이것을 시도하고 개선하기로 결정했습니다.

Callin 스레드가 스레드 ID를 "알지 못하고"스레드가 이미 사용 된 경우> 65% 감소를 할 때 동등한 작업을 위해 CPU 시간이 50% 감소하는 작은 API를 제공하는 작은 API가 있다는 것을 기쁘게 생각합니다. 스레드 -ID (아마도 다른 이전 처리 단계)를 얻었습니다.

새로운 함수 (get_thread_private_ptr ())는 항상 내부적으로 모든 종류의 구조물에 대한 포인터를 반환하므로 스레드 당 하나만 필요합니다.

대체로 Win32 TLS 지원이 실제로 제작되지 않았다고 생각합니다.

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top