web-dev-qa-db-ja.com

std :: atomicのロックはどこにありますか?

データ構造に複数の要素がある場合、そのアトミックバージョンは(常に)ロックフリーになりません。 CPUが何らかのロックを使用せずにデータをアトミックに変更することはできないため、これは大きな型には当てはまると言われました。

例えば:

#include <iostream>
#include <atomic>

struct foo {
    double a;
    double b;
};

std::atomic<foo> var;

int main()
{
    std::cout << var.is_lock_free() << std::endl;
    std::cout << sizeof(foo) << std::endl;
    std::cout << sizeof(var) << std::endl;
}

出力(Linux/gcc)は次のとおりです。

0
16
16

アトミックとfooは同じサイズであるため、ロックがアトミックに格納されるとは思わない。

私の質問は:
アトミック変数がロックを使用する場合、それはどこに格納され、その変数の複数のインスタンスにとってそれはどういう意味ですか?

67
curiousguy12

そのような質問に答える最も簡単な方法は、一般に、結果のアセンブリを見て、そこからそれを取ることです。

以下をコンパイルします(私はあなたの構造体を大きくして、craftなコンパイラーの回避策を回避しました):

#include <atomic>

struct foo {
    double a;
    double b;
    double c;
    double d;
    double e;
};

std::atomic<foo> var;

void bar()
{
    var.store(foo{1.0,2.0,1.0,2.0,1.0});
}

Clang 5.0.0では、-O3の下で次の結果が得られます。 godboltを参照

bar(): # @bar()
  sub rsp, 40
  movaps xmm0, xmmword ptr [rip + .LCPI0_0] # xmm0 = [1.000000e+00,2.000000e+00]
  movaps xmmword ptr [rsp], xmm0
  movaps xmmword ptr [rsp + 16], xmm0
  movabs rax, 4607182418800017408
  mov qword ptr [rsp + 32], rax
  mov rdx, rsp
  mov edi, 40
  mov esi, var
  mov ecx, 5
  call __atomic_store

コンパイラーは組み込み(__atomic_store)に委任しますが、それはここで実際に何が起こっているのかを教えてくれません。ただし、コンパイラはオープンソースであるため、組み込み関数の実装を簡単に見つけることができます( https://github.com/llvm-mirror/compiler-rt/blob/master/lib/builtins /atomic.c ):

void __atomic_store_c(int size, void *dest, void *src, int model) {
#define LOCK_FREE_ACTION(type) \
    __c11_atomic_store((_Atomic(type)*)dest, *(type*)dest, model);\
    return;
  LOCK_FREE_CASES();
#undef LOCK_FREE_ACTION
  Lock *l = lock_for_pointer(dest);
  lock(l);
  memcpy(dest, src, size);
  unlock(l);
}

魔法はlock_for_pointer()で発生するようですので、見てみましょう:

static __inline Lock *lock_for_pointer(void *ptr) {
  intptr_t hash = (intptr_t)ptr;
  // Disregard the lowest 4 bits.  We want all values that may be part of the
  // same memory operation to hash to the same value and therefore use the same
  // lock.  
  hash >>= 4;
  // Use the next bits as the basis for the hash
  intptr_t low = hash & SPINLOCK_MASK;
  // Now use the high(er) set of bits to perturb the hash, so that we don't
  // get collisions from atomic fields in a single object
  hash >>= 16;
  hash ^= low;
  // Return a pointer to the Word to use
  return locks + (hash & SPINLOCK_MASK);
}

そして、ここに私たちの説明があります:アトミックのアドレスは、事前に割り当てられたロックを選択するハッシュキーを生成するために使用されます。

46
Frank

通常の実装は、アトミックオブジェクトのアドレスをキーとして使用する、ミューテックスのハッシュテーブル(または、OSアシストのスリープ/ウェイクアップへのフォールバックのない単純なスピンロックですら)です。ハッシュ関数は、アドレスの下位ビットを2のべき乗サイズの配列へのインデックスとして使用するだけの単純なものかもしれませんが、@ Frankの答えは、LLVMのstd :: atomic実装がXORを示していますオブジェクトが2のべき乗(他のランダムな配置よりも一般的)で区切られている場合、自動的にエイリアシングを取得しません。

私は、g ++とclang ++がABI互換であることを確信しています(しかし、確信はありません)。つまり、彼らは同じハッシュ関数とテーブルを使用するので、どのロックがどのオブジェクトへのアクセスをシリアル化するかについて同意します。ただし、ロックはすべてlibatomicで行われるため、libatomicを動的にリンクすると、__atomic_store_16を呼び出す同じプログラム内のすべてのコードは同じ実装を使用します。 clang ++とg ++は、どの関数名を呼び出すかについて明確に同意しています。それで十分です。 (ただし、異なるプロセス間で共有メモリにあるロックフリーのアトミックオブジェクトのみが機能することに注意してください。各プロセスには、独自のロックのハッシュテーブルがあります。オブジェクトは、リージョンが異なるアドレスにマッピングされている場合でも、通常のCPUアーキテクチャの共有メモリで動作するはずです(実際に動作します)。

ハッシュ衝突は、2つのアトミックオブジェクトが同じロックを共有する可能性があることを意味します。これは正確性の問題ではありませんが、パフォーマンスの問題である可能性があります:2つの異なるオブジェクトに対して互いに競合する2つのスレッドのペアの代わりに、4つのスレッドすべてがアクセスに競合する可能性がありますどちらかのオブジェクト。おそらくそれは珍しいことであり、通常は、関心のあるプラットフォームでアトミックオブジェクトをロックフリーにすることを目指しています。しかし、ほとんどの場合、あなたは本当に不運にならず、基本的には問題ありません。

デッドロックは不可能です。2つのオブジェクトを一度にロックしようとするstd::atomic関数がないためです。そのため、ロックを取得するライブラリコードは、これらのロックの1つを保持しているときに別のロックを取得しようとしません。余分な競合/シリアル化は正確性の問題ではなく、パフォーマンスだけです。


GCC vs. MSVCを使用したx86-64 16バイトオブジェクト

ハックとして、コンパイラはlock cmpxchg16bを使用して16バイトのアトミックロード/ストア、および実際の読み取り-変更-書き込み操作を実装できます。

これはロックよりも優れていますが、8バイトのアトミックオブジェクトと比較してパフォーマンスが低下します(純粋なロードが他のロードと競合するなど)。これは、16バイトでアトミックに何かを行う唯一の文書化された安全な方法です1

知る限り、MSVCは16バイトのオブジェクトにlock cmpxchg16bを使用することはありません。これらは基本的に24または32バイトのオブジェクトと同じです。

gcc6以前のインライン lock cmpxchg16b-mcx16でコンパイルする場合(残念ながら、x86-64のcmpxchg16bはベースラインではありません。第1世代のAMD K8 CPUにはありません。)

gcc7は、常にlibatomicを呼び出し、16バイトのオブジェクトをロックフリーとして報告しないことを決定しました。ただし、命令が使用可能なマシンでは、libatomic関数は引き続きlock cmpxchg16bを使用します。 is_lock_free()がMacPorts gcc 7.3へのアップグレード後にfalseを返した を参照してください。この変更を説明するgccメーリングリストメッセージ こちら

ユニオンハックを使用して、gcc/clangを使用してx86-64で合理的に安価なABAポインター+カウンターを取得できます。 c ++ 11 CASでABAカウンターを実装するにはどうすればよいですか?lock cmpxchg16bはポインターとカウンターの両方を更新しますが、ポインターのみの単純なmovロードを行います。ただし、これは、lock cmpxchg16bを使用して16バイトのオブジェクトが実際にロックフリーの場合にのみ機能します。


脚注1movdqa 16バイトのロード/ストアは、一部では実際にはアトミックです(ただし、notall)x86マイクロアーキテクチャ。使用可能であるかどうかを検出するための信頼できる方法または文書化された方法はありません。 K10 Opteronがティアリングを示す例については、 x86の自然に位置合わせされた変数の整数割り当てはなぜですか? 、および SSE命令:アトミック16Bメモリ操作を実行できるCPUはどれですか? を参照してくださいHyperTransportを使用したソケット間の8B境界のみ。

そのため、コンパイラの作成者は注意を怠って、movdqaを32ビットコードの8バイトのアトミックロード/ストアにSSE2 movqを使用する方法で使用することはできません。 CPUベンダーが一部のマイクロアーキテクチャの保証を文書化できるか、アトミック16、32、および64バイトのアライメントされたベクターロード/ストア(SSE、AVX、およびAVX512を使用)にCPUID機能ビットを追加できると便利です。キャッシュライン全体をアトミックに転送しない特別なコヒーレンシグルーチップを使用するファンキーな多ソケットマシンのファームウェアでは、どのモボベンダーが無効にできるのでしょうか。

62
Peter Cordes

C++標準の29.5.9から:

注:アトミック特殊化の表現は、対応する引数タイプと同じサイズである必要はありません。スペシャライゼーションは、既存のコードを移植するために必要な労力を削減するため、可能な限り同じサイズにする必要があります。 —終了ノート

必須ではありませんが、アトミックのサイズをその引数型のサイズと同じにすることが望ましいです。これを実現する方法は、ロックを回避するか、ロックを別の構造に保存することです。他の答えがすでに明確に説明されているように、ハッシュテーブルはすべてのロックを保持するために使用されます。これは、使用中のすべてのアトミックオブジェクトの任意の数のロックを格納する最もメモリ効率の高い方法です。

11
Hadi Brais