38

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

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

4

6 に答える 6

38

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

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

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

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

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

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

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

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

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

于 2009-02-03T06:06:59.420 に答える
12

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 つだけです。

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です。

于 2011-04-13T08:56:39.717 に答える
9

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

tls に対して生成されるコードの種類を確認するには、次のコードをコンパイルして obj2asm を実行します。

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

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

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

于 2009-02-03T20:15:04.213 に答える
4

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

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

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

利用方法:

タイプ*: tl_ptr<タイプ>

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

for 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);
    }
};
于 2010-02-27T05:44:04.130 に答える
2

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

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

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

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

于 2009-12-18T12:45:29.370 に答える