3

次のプログラムを検討してください。

#include <pthread.h>

static int final_value = 0;

#ifdef TLS_VAR
static int __thread tls_var;
#else
static int tls_var;
#endif

void  __attribute__ ((noinline)) modify_tls(void) {
  tls_var++;
}

void *thread_function(void *unused) {
  const int iteration_count = 1 << 25;

  tls_var = 0;
  for (int i = 0; i < iteration_count; i++) {
    modify_tls();
  }
  final_value += tls_var;
  return NULL;
}

int main() {
  const int thread_count = 1 << 7;

  pthread_t thread_ids[thread_count];
  for (int i = 0; i < thread_count; i++) {
    pthread_create(&thread_ids[i], NULL, thread_function, NULL);
  }

  for (int i = 0; i < thread_count; i++) {
    pthread_join(thread_ids[i], NULL);
  }

  return 0;
}

TLS_VAR私の i7 では、定義済みの実行に 1.308 秒、未定義の実行に 8.392 秒かかります。私はそのような大きな違いを説明することはできません。

のアセンブリはmodify_tls次のようになります (異なる部分についてのみ言及しました)。

;; !defined(TLS_VAR)
movl tls_var(%rip), %eax
addl $1, %eax
movl %eax, tls_var(%rip)

;; defined(TLS_VAR)
movl %fs:tls_var@tpoff, %eax
addl $1, %eax
movl %eax, %fs:tls_var@tpoff

TLS ルックアップは、TCB からの負荷で理解できます。しかしtls_var、最初のケースの負荷が に対して相対的なのはなぜ%ripですか? ローダーによって再配置される直接メモリアドレスにできないのはなぜですか? この%rip相対的な負荷が遅さの原因ですか? もしそうなら、なぜですか?

コンパイル フラグ:gcc -O3 -std=c99 -Wall -Werror -lpthread

4

1 に答える 1

4

__thread属性がないtls_var場合は、単なる共有変数です。1 つのスレッドが書き込みを行うと、その書き込みは最初にコアのキャッシュに送られ、そこでスレッドが実行されます。ただし、これは共有変数であり、x86 マシンはキャッシュ コヒーレントであるため、他のコアのキャッシュは無効になり、それらのコンテンツは最終レベル キャッシュまたはメイン メモリから更新されます (この場合、最終レベル キャッシュから可能性が最も高い)。これは Core i7 の共有 L3 キャッシュです)。最終レベル キャッシュはメイン メモリより高速ですが、無限に高速ではないことに注意してください。そこから L2 および L1 キャッシュにデータを移動し、各コアにプライベートに移動するには、依然として多くのサイクルが必要です。

属性を使用すると、各スレッドは、スレッド ローカル ストレージにある__threadの独自のコピーを取得します。tls_varこれらのスレッド ローカル ストレージはメモリ内で互いに離れているため、変更時にキャッシュ コヒーレンシ メッセージは関与せず、データは最速の L1 キャッシュに留まります。

RIP関連するアドレッシング (「近い」データの x64 デフォルト アドレッシング モードの System V ABI で推奨されている) は、通常、データ アクセスを高速化しますが、キャッシュ コヒーレンシ オーバーヘッドが非常に大きいため、低速の TLS アクセスは、すべてが保持されている場合は実際には高速です。 L1 キャッシュ。

この問題は、マルチプロセッサ (ポスト) Nehalem や AMD64 ボードなどの NUMA システムでは非常に大きくなります。キャッシュの一貫性を保つにはコストがかかるだけでなく、変数に最初に「触れた」スレッドが存在するソケットに接続されたメモリに共有変数が存在することになります。他のソケットのコアで実行されるスレッドは、ソケットを接続する QPI または HT バスを介してリモート メモリ アクセスを実行する必要があります。ある客員教授が最近言ったように (大まかな言い換え)、「共有メモリ システムは、分散メモリ システムであるかのようにプログラムします」。これには、作業対象のグローバル データのローカル コピーを作成することが含まれます。これはまさに__thread属性が達成することです。

またtls_var、TLS に参加している場合と参加していない場合では、異なる結果が予想されることに注意してください。TLS にあるため、1 つのスレッドが行った変更は他のスレッドには表示されません。共有変数であるため、特定の時間に複数のスレッドがアクセスできないようにする必要があります。これは通常、クリティカル セクションまたはロックされた追加で実現されます。

于 2012-12-03T20:02:50.203 に答える