web-dev-qa-db-ja.com

スレッドローカルストレージが非常に遅いのはなぜですか?

私は、スレッドローカル領域から割り当てることによって機能するDプログラミング言語用のカスタムマークリリーススタイルのメモリアロケータに取り組んでいます。スレッドローカルストレージのボトルネックにより、割り当てごとにTLSルックアップが1つだけになるようにコードを設計した後でも、他の点では同一のシングルスレッドバージョンのコードと比較して、これらの領域からのメモリの割り当てが大幅に(〜50%)遅くなっているようです。割り当て解除。これは、ループ内でメモリを何度も割り当て/解放することに基づいており、それが私のベンチマーク方法の成果物であるかどうかを把握しようとしています。私の理解では、スレッドローカルストレージは基本的に、ポインターを介して変数にアクセスするのと同様に、間接参照の追加レイヤーを介して何かにアクセスすることを含む必要があります。これは間違っていますか?スレッドローカルストレージには通常どのくらいのオーバーヘッドがありますか?

注:Dについて言及しましたが、Dに固有ではない一般的な回答にも興味があります。これは、スレッドローカルストレージのDの実装は、最良の実装よりも遅い場合に改善される可能性があるためです。

36
dsimcha

速度はTLSの実装によって異なります。

はい、あなたはTLSがポインタルックアップと同じくらい速くなることができるということは正しいです。メモリ管理ユニットを備えたシステムでは、さらに高速になる可能性があります。

ただし、ポインタルックアップについては、スケジューラからの支援が必要です。スケジューラーは、タスクスイッチでTLSデータへのポインターを更新する必要があります。

TLSを実装するもう1つの高速な方法は、メモリ管理ユニットを使用することです。ここでは、TLS変数が特別なセグメントに割り当てられていることを除いて、TLSは他のデータと同じように扱われます。スケジューラは、タスクスイッチで、メモリの正しいチャンクをタスクのアドレス空間にマップします。

スケジューラーがこれらのメソッドのいずれもサポートしていない場合、コンパイラー/ライブラリーは以下を実行する必要があります。

  • 現在のThreadIdを取得します
  • セマフォを取る
  • ThreadIdでTLSブロックへのポインターを検索します(マップなどを使用する場合があります)
  • セマフォを解放します
  • そのポインタを返します。

明らかに、TLSデータアクセスごとにこれらすべてを実行するには時間がかかり、最大3つのOS呼び出しが必要になる場合があります:ThreadIdの取得、セマフォの取得と解放。

セマフォは、別のスレッドが新しいスレッドを生成している最中に、TLSポインターリストからスレッドが読み取られないようにするために必要です。 (したがって、新しいTLSブロックを割り当て、データ構造を変更します)。

残念ながら、実際には遅いTLS実装を見るのは珍しいことではありません。

34

Dのスレッドローカルは本当に高速です。これが私のテストです。

64ビットUbuntu、コアi5、dmd v2.052コンパイラオプション:dmd -O -release -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つが1.2秒しか失われません。スレッドローカルは%fsレジスタを使用してアクセスされるため、関係するプロセッサコマンドは2、3個だけです。

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コンパイラと比較するのは興味深いです)が、今でも問題は非常に優れています。

10
Andriy

ベンチマーク結果の解釈には非常に注意する必要があります。たとえば、Dニュースグループの最近のスレッドは、ベンチマークから、dmdのコード生成が算術演算を行うループで大幅な速度低下を引き起こしていると結論付けましたが、実際に費やされる時間は、筆算を行うランタイムヘルパー関数によって支配されていました。コンパイラのコード生成は、速度低下とは何の関係もありませんでした。

Tlsに対してどのような種類のコードが生成されるかを確認するには、次のコードをコンパイルしてobj2asmします。

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

TLSは、WindowsとLinuxで実装が大きく異なり、OSXでも大きく異なります。ただし、すべての場合において、静的メモリ位置の単純なロードよりもはるかに多くの命令になります。 TLSは、単純なアクセスに比べて常に遅くなります。タイトループでのTLSグローバルへのアクセスも遅くなります。代わりに、TLS値を一時的にキャッシュしてみてください。

私は数年前にいくつかのスレッドプール割り当てコードを作成し、TLSハンドルをプールにキャッシュしました。これはうまく機能しました。

8
Walter Bright

コンパイラーTLSサポートを使用できない場合は、TLSを自分で管理できます。 C++のラッパーテンプレートを作成したので、基盤となる実装を簡単に置き換えることができます。この例では、Win32用に実装しました。注:プロセスごとに無制限の数のTLSインデックスを取得することはできないため(少なくともWin32では)、すべてのスレッド固有のデータを保持するのに十分な大きさのヒープブロックを指定する必要があります。このようにして、TLSインデックスと関連するクエリの数を最小限に抑えることができます。 「最良の場合」では、スレッドごとに1つのプライベートヒープブロックを指すTLSポインターが1つだけあります。

簡単に言うと、単一のオブジェクトを指すのではなく、スレッド固有のヒープメモリ/オブジェクトポインタを保持するコンテナを指すようにして、パフォーマンスを向上させます。

再度使用しない場合は、メモリを解放することを忘れないでください。これを行うには、スレッドをクラスにラップし(Java dos)のように)、コンストラクタとデストラクタによってTLSを処理します。さらに、スレッドハンドルやIDなどの頻繁に使用されるデータをクラスメンバーとして格納します。

使用法:

タイプ*の場合:tl_ptr <type>

const type *の場合:tl_ptr <const type>

type * constの場合:const tl_ptr <type>

const type * const:const tl_ptr <const type>

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);
    }
};
4
sam

私は組み込みシステム用のマルチタスカーを設計しました。概念的には、スレッドローカルストレージの重要な要件は、コンテキストスイッチメソッドに、CPUレジスタやその他の保存/復元とともにスレッドローカルストレージへのポインタを保存/復元させることです。起動後に常に同じコードセットを実行する組み込みシステムの場合、各スレッドの固定形式ブロックを指す1つのポインターを保存/復元するのが最も簡単です。素晴らしく、清潔で、簡単で、効率的です。

このようなアプローチは、すべてのスレッド内に割り当てられたすべてのスレッドローカル変数(実際には使用しない変数も含む)にスペースがあることを気にせず、スレッドローカルストレージブロック内にあるすべてのものを単一の構造体として定義されます。そのシナリオでは、スレッドローカル変数へのアクセスは他の変数へのアクセスとほぼ同じくらい高速である可能性がありますが、唯一の違いは追加のポインター逆参照です。残念ながら、多くのPCアプリケーションはもっと複雑なものを必要とします。

PCの一部のフレームワークでは、スレッドの静的変数を使用するモジュールがそのスレッドで実行されている場合にのみ、スレッドの静的変数にスペースが割り当てられます。これは有利な場合もありますが、スレッドが異なれば、ローカルストレージのレイアウトも異なることがよくあります。したがって、スレッドには、変数が配置されている場所の検索可能なインデックスがあり、そのインデックスを介してそれらの変数へのすべてのアクセスを指示する必要がある場合があります。

フレームワークが少量の固定形式のストレージを割り当てる場合、多くのシナリオで単一アイテムのキャッシュでも提供できるため、アクセスされた最後の1〜3個のスレッドローカル変数のキャッシュを保持すると役立つ場合があります。かなり高いヒット率。

4
supercat

TLS(Windows上)でも同様のパフォーマンスの問題が発生しています。私たちは、製品の「カーネル」内の特定の重要な操作をこれに依存しています。少し努力した後、これを改善しようと決心しました。

呼び出しスレッドがスレッドIDを「認識」していない場合、同等の操作でCPU時間を50%以上削減し、スレッドを呼び出している場合は65%以上削減できる小さなAPIができたことを嬉しく思います。そのスレッドIDを取得しました(おそらく他の初期の処理ステップのために)。

新しい関数(get_thread_private_ptr())は、すべての種類を保持するために内部で使用する構造体へのポインターを常に返すため、スレッドごとに1つだけ必要です。

全体として、Win32TLSサポートは実際には不十分に作成されていると思います。

2
Hugh