LLVMアトミック命令と並行性ガイド

はじめに

LLVMは、スレッドと非同期シグナルが存在する場合でも明確に定義された命令をサポートしています。

アトミック命令は、特に以下の場合に読みやすいIRと最適化されたコード生成を提供するために設計されています。

  • C++の<atomic>ヘッダーとCの<stdatomic.h>ヘッダー。これらは元々C++11とC11で追加されました。メモリモデルは、初期仕様のエラーを修正するためにその後調整されているため、LLVMは現在C++20で指定されたバージョンを実装することを意図しています。(C++20ドラフト標準または非公式の最新のC++ドラフトを参照してください。C2xドラフトも入手できますが、テキストはまだC++20で修正された正誤表で更新されていません。)

  • volatileと通常の共有変数の両方について、Javaスタイルのメモリの適切なセマンティクスを提供します。(Java仕様

  • gcc互換の__sync_*組み込み関数。(説明

  • C++における非自明なコンストラクタを持つstatic変数を含む、アトミックセマンティクスを持つ他のシナリオ。

IRにおけるアトミックとvolatileは直交しています。「volatile」はC/C++のvolatileであり、すべてのvolatileロードとストアが発生し、指定された順序で実行されることを保証します。いくつかの例:SequentiallyConsistentストアの直後に同じアドレスへの別のSequentiallyConsistentストアが続く場合、最初のストアは削除できます。この変換は、volatileストアのペアには許可されていません。一方、非volatileの非アトミックロードはvolatileロードを自由に移動できますが、Acquireロードは移動できません。

このドキュメントは、LLVMのフロントエンドを作成する人、またはLLVMの最適化パスに取り組む人のための、並行性がある場合に特別なセマンティクスを持つ命令を処理する方法のガイドを提供することを目的としています。これは、セマンティクスの正確なガイドとなることを意図したものではありません。詳細は非常に複雑で読みにくくなる可能性があり、通常は必要ありません。

アトミック命令外部の最適化

基本的な'load''store'はさまざまな最適化を可能にしますが、並行環境では未定義の結果につながる可能性があります。NotAtomicを参照してください。このセクションでは、並行環境に適用される1つのオプティマイザの制限について具体的に説明します。これは、ストアを扱う最適化はすべてこれを認識する必要があるため、少し詳しく説明します。

オプティマイザの観点からは、アトミック順序付けが関係する命令がない場合、並行性は問題にならないというルールがあります。ただし、1つの例外があります。変数が別のスレッドまたはシグナルハンドラから見える可能性がある場合、ストアは、そうでなければ実行されない可能性のあるパスに挿入することはできません。次の例を考えてみましょう。

/* C code, for readability; run through clang -O2 -S -emit-llvm to get
    equivalent IR */
 int x;
 void f(int* a) {
   for (int i = 0; i < 100; i++) {
     if (a[i])
       x += 1;
   }
 }

以下は、非並行状況では同等です。

int x;
void f(int* a) {
  int xtemp = x;
  for (int i = 0; i < 100; i++) {
    if (a[i])
      xtemp += 1;
  }
  x = xtemp;
}

ただし、LLVMは前者を後者に変換することはできません。別のスレッドが同時にxにアクセスできる場合、間接的に未定義の動作が発生する可能性があります。そのスレッドは、期待していた値の代わりにundefを読み取り、後で未定義の動作につながる可能性があります。(この例は、並行性モデルが実装される前はLLVMがこの変換を実行していたため、特に興味深いものです。)

投機的ロードは許可されていることに注意してください。競合の一部であるロードはundefを返しますが、未定義の動作はありません。

アトミック命令

単純なロードとストアでは不十分な場合、LLVMはさまざまなアトミック命令を提供します。提供される正確な保証は、順序付けによって異なります。アトミック順序付けを参照してください。

load atomicstore atomicは、非アトミックロードとストアと同じ基本機能を提供しますが、スレッドとシグナルが関係する場合に追加の保証を提供します。

cmpxchgatomicrmwは、基本的にアトミックロードの後にアトミックストアが続く 것과 같습니다(cmpxchgの場合はストアは条件付きです)。ただし、ロードとストアの間に他のメモリ操作がスレッドで発生することはありません。

fenceは、別の操作の一部ではないAcquireまたはReleaseの順序付け(あるいは両方)を提供します。通常、Monotonicメモリ操作と一緒に使用されます。Monotonicロードの後にAcquireフェンスが続くのは、ほぼAcquireロードと同等であり、Releaseフェンスが続くMonotonicストアは、ほぼReleaseストアと同等です。SequentiallyConsistentフェンスは、AcquireフェンスとReleaseフェンスの両方として動作し、さらに複雑な保証を備えた全体的な順序付けを提供します。詳細はC++標準を参照してください。

アトミック命令を生成するフロントエンドは、一般的にターゲットをある程度認識する必要があります。アトミック命令はロックフリーであることが保証されているため、ターゲットがネイティブにサポートするよりも幅広い命令は生成できません。

アトミック順序付け

パフォーマンスと必要な保証のバランスをとるために、6レベルの原子性があります。強度の順にリストされています。各レベルには、Acquire/Releaseを除く、前のレベルのすべての保証が含まれています。(LangRef Orderingも参照してください。)

NotAtomic

NotAtomicは明白で、アトミックではないロードまたはストアです。(これは実際には原子性のレベルではありませんが、比較のためにここにリストされています。)これは本質的に通常のロードまたはストアです。特定のメモリロケーションで競合が発生した場合、そのロケーションからのロードはundefを返します。

関連する標準

これは、C/C++の共有変数に一致し、メモリアクセスが必要であり、競合が発生しない他のコンテキストで使用することを目的としています。(正確な定義は、LangRef Memory Modelにあります。)

フロントエンド向けの注意事項

ルールは本質的に、複数のスレッドによって基本的なロードとストアでアクセスされるすべてのメモリは、ロックまたは他の同期によって保護される必要があるということです。そうでない場合、未定義の動作が発生する可能性があります。フロントエンドがJavaのような「安全な」言語の場合は、Unorderedを使用して共有変数をロードおよびストアします。NotAtomic volatileロードとストアは適切にアトミックではないことに注意してください。代用として使用しようとしないでください。(C/C++標準では、volatileは非同期シグナルに関する限定的な保証を提供しますが、アトミックは一般的に優れたソリューションです。)

オプティマイザ向けの注意事項

存在しないはずのコードパスに共有変数へのロードを導入することは許可されています。共有変数へのストアを導入することは許可されていません。アトミック命令外部の最適化を参照してください。

コード生成向けの注意事項

ここで興味深い制限の1つは、ストアに関連するバイト以外のバイトに書き込むことができないことです。これは主にアライメントされていないストアに関連しています。一般に、アライメントされていないストアを、アライメントされていないストアと同じ幅の2つのアライメントされたストアに変換することはできません。また、バックエンドはi8ストアをi8ストアとして生成することが期待されており、周囲のバイトに書き込む命令としては生成されません。(これらの制限を満たすことができず、並行性を気にするアーキテクチャのバックエンドを作成している場合は、llvm-devにメールを送信してください。)

Unordered

Unorderedは、最低レベルの原子性です。これは本質的に、競合が発生した場合に未定義の動作ではなく、ある程度健全な結果が生成されることを保証します。また、操作がロックフリーであることも保証するため、データが特別なアトミック構造の一部であること、または個別のプロセスごとのグローバルロックに依存することはありません。サポートされていないアトミック操作では、コード生成が失敗することに注意してください。そのような操作が必要な場合は、明示的なロックを使用してください。

関連する標準

これは、共有変数に対するJavaメモリモデルに一致することを目的としています。

フロントエンド向けの注意事項

これは同期には使用できませんが、生成されたコードが未定義の動作を示さないことを保証する必要があるJavaなどの「安全な」言語に役立ちます。この保証は、ネイティブ幅のロードでは一般的なプラットフォームでは安価ですが、ARMでの64ビットストアなど、幅の広いロードでは高価になるか、利用できない場合があることに注意してください。(Javaまたは他の「安全な」言語のフロントエンドは、通常、ARMでの64ビットストアを2つの32ビットの順序付けられていないストアに分割します。)

オプティマイザ向けの注意事項

オプティマイザの観点から見ると、これは、単一のロードを複数のロードに変換する変換、ストアを複数のストアに変換する変換、ストアを狭める変換、またはそうでなければストアされない値をストアする変換を禁止します。安全でない最適化の例としては、ビットフィールドへの代入の絞り込み、ロードの再マテリアライズ、ロードとストアのmemcpy呼び出しへの変換などがあります。ただし、順序付けられていない操作の並べ替えは安全であり、順序付けられていない操作はそれらを必要とする言語で一般的であるため、オプティマイザはそれを活用する必要があります。

コード生成向けの注意事項

これらの操作は、順序付けされていないロードとストアを使用する場合、ロードがストアされていない値を参照できないという意味で、アトミックである必要があります。 通常のロードまたはストア命令で十分ですが、順序付けされていないロードまたはストアは、複数の命令(または、LPAEのないARMのLDRD、またはLPAE ARMの自然にアラインされていないLDRDのように、複数のメモリオペレーションを実行する命令)に分割できないことに注意してください。

単調性(Monotonic)

単調性(Monotonic)は、同期プリミティブで使用できる最も弱いレベルの原子性ですが、一般的な同期は提供しません。 基本的に、特定のアドレスに影響を与えるすべての操作を実行すると、一貫した順序が存在することを保証します。

関連する標準

これは、C ++ / Cのmemory_order_relaxedに対応します。 正確な定義については、これらの標準を参照してください。

フロントエンド向けの注意事項

これを直接使用するフロントエンドを作成する場合は、注意して使用してください。 同期の保証は非常に弱いため、これらが正しいことがわかっているパターンでのみ使用されていることを確認してください。 一般的に、これらは、他のメモリを保護しないアトミック操作(アトミックカウンターなど)、またはfenceと共に使用されます。

オプティマイザ向けの注意事項

オプティマイザの観点から、これは関連するメモリ位置の読み取り+書き込みとして扱うことができます(エイリアス分析はそれを利用します)。 さらに、非アトミックおよび順序付けされていないロードを単調ロードの周囲で並べ替えることは合法です。 CSE / DSEといくつかの他の最適化は許可されていますが、単調操作はこれらの最適化を有効にする方法で使用される可能性は低いです。

コード生成向けの注意事項

コード生成は、基本的にロードとストアの順序付けられていないものと同じです。 フェンスは必要ありません。 cmpxchgatomicrmwは、単一の操作として表示される必要があります。

獲得(Acquire)

獲得(Acquire)は、通常のロードとストアで他のメモリにアクセスするためにロックを取得するために必要な種類のバリアを提供します。

関連する標準

これは、C ++ / Cのmemory_order_acquireに対応します。 C ++ / Cのmemory_order_consumeにも使用する必要があります。

フロントエンド向けの注意事項

これを直接使用するフロントエンドを作成する場合は、注意して使用してください。 獲得(Acquire)は、解放(Release)操作と組み合わせて使用​​する場合にのみセマンティック保証を提供します。

オプティマイザ向けの注意事項

アトミックを認識しないオプティマイザは、これをnothrow呼び出しのように扱うことができます。 また、獲得ロードまたは読み取り-変更-書き込み操作の前のストアをその後に移動し、獲得操作の前の非獲得ロードをその後に移動することもできます。

コード生成向けの注意事項

弱いメモリ順序付けを持つアーキテクチャ(基本的にx86とSPARCを除く今日のすべて関連するもの)は、獲得セマンティクスを維持するために何らかのフェンスが必要です。 必要な正確なフェンスはアーキテクチャによって大きく異なりますが、単純な実装では、ほとんどのアーキテクチャはすべてに十分な強度を持つバリアを提供します(ARMのdmb、PowerPCのsyncなど)。 同等の単調操作の後にそのようなフェンスを配置するだけで、メモリオペレーションの獲得セマンティクスを維持できます。

解放(Release)

解放(Release)は獲得(Acquire)に似ていますが、ロックを解放するために必要な種類のバリアがあります。

関連する標準

これは、C ++ / Cのmemory_order_releaseに対応します。

フロントエンド向けの注意事項

これを直接使用するフロントエンドを作成する場合は、注意して使用してください。 解放(Release)は、獲得(Acquire)操作と組み合わせて使用​​する場合にのみセマンティック保証を提供します。

オプティマイザ向けの注意事項

アトミックを認識しないオプティマイザは、これをnothrow呼び出しのように扱うことができます。 また、解放ストアまたは読み取り-変更-書き込み操作の後のロードをその前に移動し、解放操作の後の非解放ストアをその前に移動することもできます。

コード生成向けの注意事項

獲得に関するセクションを参照してください。 関連する操作の前にフェンスを配置するだけで、通常は解放で十分です。 ストア-ストアフェンスは解放セマンティクスを実装するのに十分ではないことに注意してください。 ストア-ストアフェンスは、正しく使用するのが非常に難しいため、一般的にIRに公開されません。

獲得解放(AcquireRelease)

獲得解放(AcquireRelease)(IRではacq_rel)は、獲得バリアと解放バリアの両方を提供します(メモリを読み書きするフェンスと操作の場合)。

関連する標準

これは、C ++ / Cのmemory_order_acq_relに対応します。

フロントエンド向けの注意事項

これを直接使用するフロントエンドを作成する場合は、注意して使用してください。 獲得は、解放操作と組み合わせて使用​​する場合にのみセマンティック保証を提供し、逆もまた同様です。

オプティマイザ向けの注意事項

一般に、オプティマイザはこれをnothrow呼び出しのように扱う必要があります。 可能な最適化は通常、興味深いものではありません。

コード生成向けの注意事項

この操作には、獲得と解放のセマンティクスがあります。 獲得と解放に関するセクションを参照してください。

順次一貫性(SequentiallyConsistent)

順次一貫性(SequentiallyConsistent)(IRではseq_cst)は、ロードに獲得セマンティクスを、ストアに解放セマンティクスを提供します。 さらに、すべての順次一貫性操作の間に全体的な順序が存在することを保証します。

関連する標準

これは、C ++ / Cのmemory_order_seq_cst、Java volatile、および特に指定のないgcc互換の__sync_*組み込み関数に対応します。

フロントエンド向けの注意事項

フロントエンドがアトミック操作を公開している場合、これらはプログラマーにとって他の種類の操作よりもはるかに推論しやすく、使用することは一般的に実用的なパフォーマンスのトレードオフです。

オプティマイザ向けの注意事項

アトミックを認識しないオプティマイザは、これをnothrow呼び出しのように扱うことができます。 順次一貫性のあるロードとストアの場合、順次一貫性のある操作を並べ替えることはできないことを除いて、獲得ロードと解放ストアの場合と同じ並べ替えが許可されます。

コード生成向けの注意事項

順次一貫性のあるロードには、少なくとも獲得操作と同じバリアが必要であり、順次一貫性のあるストアには解放バリアが必要です。 さらに、コードジェネレータは、順次一貫性のあるストアとそれに続く順次一貫性のあるロードの順序を強制する必要があります。 これは通常、ロードの前に完全なフェンスを発行するか、ストアの後に完全なフェンスを発行することによって行われます。 どちらが推奨されるかはアーキテクチャによって異なります。

アトミックとIRの最適化

オプティマイザライターがクエリするための述語

  • isSimple():揮発性でもアトミックでもないロードまたはストア。 これは、たとえば、memcpyoptが変換する可能性のある操作をチェックするものです。

  • isUnordered():揮発性ではなく、最大で順序付けされていないロードまたはストア。 これは、たとえば、操作をホイストする前にLICMによってチェックされます。

  • mayReadFromMemory() / mayWriteToMemory():既存の述語ですが、揮発性であるか、少なくとも単調である操作に対してtrueを返すことに注意してください。

  • isStrongerThan / isAtLeastOrStrongerThan:これらは順序付けに関する述語です。 アトミックを認識しているパスで役立ちます。たとえば、単一のアトミックアクセス全体でDSEを実行しますが、解放-獲得ペア全体では実行しません(この例についてはMemoryDependencyAnalysisを参照)。

  • エイリアス分析:AAは、獲得または解放のもの、および単調操作によってアクセスされるアドレスに対してModRefを返すことに注意してください。

アトミック操作の周囲を最適化するために、適切な述語を使用していることを確認してください。 それが行われれば、すべてが機能するはずです。 パスが一部のアトミック操作(特に順序付けされていない操作)を最適化する必要がある場合は、アトミックロードまたはストアを非アトミック操作に置き換えないようにしてください。

最適化がさまざまな種類のアトミック操作とどのように相互作用するかについてのいくつかの例

  • memcpyopt:アトミック操作は、順序付けされていないロード/ストアを含め、memcpy / memsetの一部に最適化することはできません。 一部のアトミック操作全体で操作を引き出すことができます。

  • LICM:順序付けされていないロード/ストアはループから移動できます。 単調操作をメモリ位置への読み取り+書き込みのように扱い、それよりも厳密なものはnothrow呼び出しのように扱います。

  • DSE:順序付けされていないストアは、通常のストアのようにDSEできます。 単調ストアは場合によってはDSEできますが、推論するのは難しく、特に重要ではありません。 場合によっては、DSEがより強力なアトミック操作全体で動作する可能性がありますが、かなり注意が必要です。 DSEはこの推論をMemoryDependencyAnalysisに委任します(これはGVNなどの他のパスでも使用されます)。

  • ロードの折りたたみ:定数グローバルからのアトミックロードは、観察できないため、定数折りたたみできます。 同様の推論により、アトミックロードとストアでsroaを使用できます。

アトミックとコード生成

アトミック操作は、SelectionDAGでATOMIC_*オペコードで表されます。 すべてのアトミック順序付けにバリア命令を使用するアーキテクチャ(ARMなど)では、shouldInsertFencesForAtomic()がtrueを返す場合、AtomicExpand Codegenパスによって適切なフェンスを発行できます。

すべてのアトミック操作のMachineMemOperandは現在、揮発性としてマークされています。 これは、揮発性のIRの意味では正しくありませんが、CodeGenは揮発性としてマークされたものを非常に保守的に処理します。 これはいつか修正されるはずです。

アトミック操作の非常に重要な特性の1つは、バックエンドが特定のサイズのインラインロックフリーアトミック操作をサポートしている場合、そのサイズの*すべて*の操作をロックフリーの方法でサポートする必要があることです。

ターゲットがアトミックなcmpxchg命令またはLL/SC命令を実装している場合(ほとんどの場合そうです)、これは簡単です。他のすべての操作は、これらのプリミティブの上に実装できます。しかし、多くの古いCPU(ARMv5、SparcV8、Intel 80386など)では、アトミックなロードおよびストア命令はありますが、cmpxchgまたはLL/SCはありません。ネイティブ命令を使用してatomic loadを実装することは無効ですが、ミューテックスを使用する関数へのライブラリ呼び出しを使用してcmpxchgを実装することは有効です。そのため、atomic loadは、そのようなアーキテクチャでもライブラリ呼び出しに展開する必要があります。これにより、同じミューテックスを使用して、同時cmpxchgに関してアトミック性を維持できます。

AtomicExpandPassは、この問題に役立ちます。setMaxAtomicSizeInBitsSupported(デフォルトは0)で設定された最大値を超えるすべてのサイズのすべての原子操作を、適切な__atomic_*ライブラリ呼び出しに展開します。

x86では、すべてのアトミックロードはMOVを生成します。SequentiallyConsistentストアはXCHGを生成し、他のストアはMOVを生成します。SequentiallyConsistentフェンスはMFENCEを生成し、他のフェンスはコードを生成しません。cmpxchgLOCK CMPXCHG命令を使用します。atomicrmw xchgXCHGを使用し、atomicrmw addatomicrmw subXADDを使用し、他のすべてのatomicrmw操作はLOCK CMPXCHGを使用したループを生成します。結果の使用者によっては、一部のatomicrmw操作はLOCK ANDのような操作に変換できますが、これは一般的には機能しません。

ARM(v8より前)、MIPS、および他の多くのRISCアーキテクチャでは、Acquire、Release、およびSequentiallyConsistentセマンティクスには、そのような操作ごとにバリア命令が必要です。ロードとストアは通常の命令を生成します。cmpxchgatomicrmwは、キャッシュラインに対して何らかの排他ロックを取得するLL/SCスタイルの命令(ARMのLDREXSTREXなど)を使用したループを使用して表現できます。

バックエンドにとって、AtomicExpandPassを使用してアトミック構造の一部をLoweringするのが最も簡単な場合がよくあります。以下に、それが実行できるLoweringをいくつか示します。

  • cmpxchg -> shouldExpandAtomicCmpXchgInIR()emitLoadLinked()emitStoreConditional()をオーバーライドすることにより、ロードリンク/ストアコンディショナルを使用したループに変換します。

  • 大きなロード/ストア -> shouldExpandAtomicStoreInIR()/shouldExpandAtomicLoadInIR()をオーバーライドすることにより、ll-sc/cmpxchgに変換します。

  • 強いアトミックアクセス -> shouldInsertFencesForAtomic()emitLeadingFence()、およびemitTrailingFence()をオーバーライドすることにより、単調なアクセス+フェンスに変換します。

  • atomic rmw -> expandAtomicRMWInIR()をオーバーライドすることにより、cmpxchgまたはロードリンク/ストアコンディショナルを使用したループに変換します。

  • サポートされていないサイズの__atomic_*ライブラリ呼び出しへの展開。

  • 部分ワードatomicrmw/cmpxchg -> shouldExpandAtomicRMWInIRemitMaskedAtomicRMWIntrinsicshouldExpandAtomicCmpXchgInIR、およびemitMaskedAtomicCmpXchgIntrinsicをオーバーライドすることにより、ターゲット固有の組み込み関数に変換します。

これらの例については、ARM(最初の5つのLowering)またはRISC-V(最後のLowering)バックエンドを参照してください。

AtomicExpandPassは、atomicrmw/cmpxchgをロードリンク/ストアコンディショナル(LL/SC)ループにLoweringするための2つの戦略をサポートしています。1つ目は、LLおよびSC操作の組み込み関数を発行するためにターゲットLoweringフックを呼び出して、IRでLL/SCループを展開することです。ただし、多くのアーキテクチャでは、ループ内の命令の数と種類など、前方への進行を確保するためにLL/SCループに厳密な要件があります。LLVM IRでループを展開するときにこれらの制限を適用することはできないため、影響を受けるターゲットは、非常に遅い段階(つまり、レジスタ割り当て後)でLL/SCループに展開することを好む場合があります。AtomicExpandPassは、LL/SCループの外側で実行できるシフトとマスキングのIRを生成することにより、この戦略を使用して部分ワードatomicrmwまたはcmpxchgのLoweringをサポートできます。

ライブラリ呼び出し:__atomic_*

LLVMによって生成されるアトミックライブラリ呼び出しには2種類あります。どちらのライブラリ関数も、clangで定義されている組み込み関数の名前をやや紛らわしいことに共有していることに注意してください。それにもかかわらず、ライブラリ関数は組み込み関数に直接関連していません。__atomic_*組み込み関数が__atomic_*ライブラリ呼び出しにLoweringされ、__sync_*組み込み関数が__sync_*ライブラリ呼び出しにLoweringされるわけでは*ありません*。

最初のライブラリ関数のセットは__atomic_*という名前です。このセットはGCCによって「標準化」されており、以下で説明します。(GCCのドキュメントも参照してください)

LLVMのAtomicExpandPassは、MaxAtomicSizeInBitsSupportedを超えるデータサイズに対するアトミック操作を、これらの関数への呼び出しに変換します。

任意のサイズまたはアライメントのデータで呼び出すことができる4つの汎用関数があります。

void __atomic_load(size_t size, void *ptr, void *ret, int ordering)
void __atomic_store(size_t size, void *ptr, void *val, int ordering)
void __atomic_exchange(size_t size, void *ptr, void *val, void *ret, int ordering)
bool __atomic_compare_exchange(size_t size, void *ptr, void *expected, void *desired, int success_order, int failure_order)

上記の関数のサイズ特化バージョンもあり、適切なサイズの*自然にアライメントされた*ポインタでのみ使用できます。以下のシグネチャでは、「N」は1、2、4、8、および16のいずれかであり、「iN」はそのサイズの適切な整数型です。そのような整数型が存在しない場合、特殊化は使用できません。

iN __atomic_load_N(iN *ptr, iN val, int ordering)
void __atomic_store_N(iN *ptr, iN val, int ordering)
iN __atomic_exchange_N(iN *ptr, iN val, int ordering)
bool __atomic_compare_exchange_N(iN *ptr, iN *expected, iN desired, int success_order, int failure_order)

最後に、サイズ固有のバリアントでのみ使用できる読み取り-変更-書き込み関数があります(他のサイズは__atomic_compare_exchangeループを使用します)。

iN __atomic_fetch_add_N(iN *ptr, iN val, int ordering)
iN __atomic_fetch_sub_N(iN *ptr, iN val, int ordering)
iN __atomic_fetch_and_N(iN *ptr, iN val, int ordering)
iN __atomic_fetch_or_N(iN *ptr, iN val, int ordering)
iN __atomic_fetch_xor_N(iN *ptr, iN val, int ordering)
iN __atomic_fetch_nand_N(iN *ptr, iN val, int ordering)

このライブラリ関数のセットには、注意すべき興味深い実装要件がいくつかあります。

  • 既存のハードウェアではネイティブに実装できないサイズ/アライメントを含め、すべてのサイズとアライメントをサポートしています。したがって、一部のサイズ/アライメントでは、ミューテックスを使用することがあります。

  • その結果、プログラムにロードされているすべてのDSO間で共有する必要がある状態があるため、静的にリンクされたコンパイラサポートライブラリに同梱することはできません。すべてのオブジェクトが使用する共有ライブラリで提供する必要があります。

  • ロックフリーでサポートされるアトミックサイズのセットは、コンパイラが発行できるサイズのスーパーセットである必要があります。つまり、新しいコンパイラがサイズNのインラインロックフリーアトミックのサポートを導入する場合、__atomic_*関数もサイズNのロックフリー実装を持っている必要があります。これは、古いコンパイラ(__atomic_*関数を呼び出します)によって生成されたコードが、新しいコンパイラ(ネイティブアトミック命令を使用します)によって生成されたコードと相互運用できるようにするための要件です。

サポートされているサイズの自然にアライメントされたポインタに対する操作を実装するためにコンパイラアトミック組み込み関数自体を使用し、それ以外の場合は汎用ミューテックス実装を使用することにより、これらのライブラリ関数の完全にターゲットに依存しない実装を記述できることに注意してください。

ライブラリ呼び出し:__sync_*

一部のターゲットまたはOS /ターゲットの組み合わせは、ロックフリーアトミックをサポートできますが、さまざまな理由により、命令をインラインで発行することは実用的ではありません。

これには2つの典型的な例があります。

一部のCPUは、関数呼び出し境界で切り替えることができる複数の命令セットをサポートしています。たとえば、MIPSは、通常のMIPS32 ISAよりも小さい命令エンコーディングを持つMIPS16 ISAをサポートしています。同様に、ARMにはThumb ISAがあります。MIPS16および以前のバージョンのThumbでは、アトミック命令はエンコードできません。ただし、これらの命令は、より長いエンコーディングを持つ関数への関数呼び出しを介して使用できます。

さらに、少数のOS /ターゲットペアは、カーネルでサポートされるロックフリーアトミックを提供します。ARM / Linuxは、これの例です。カーネルは提供します古いCPUでは「魔法のように再起動可能な」アトミックシーケンス(CPUが1つしかない限りアトミックに見えます)を含む関数と、新しいマルチコアモデルの実際のアトミック命令を含みます。この種の機能は、アトミック比較交換をサポートしていないすべてのCPUがユニプロセッサ(SMPなし)である場合、通常はどのアーキテクチャでも提供できます。これはほとんどの場合です。そのプロパティを持たない唯一の一般的なアーキテクチャはSPARCです。SPARCV8 SMPシステムは一般的でしたが、いかなる種類の比較と交換操作もサポートしていません。

一部のターゲット(RISCVなど)は、+forced-atomicsターゲット機能をサポートしています。これにより、LLVMが特定のOSサポートを認識していない場合でも、ロックフリーアトミックを使用できます。この場合、ユーザーは必要な__sync_*実装が利用可能であることを確認する責任があります。アトミック変数がABI境界を越える場合、+forced-atomicsを使用するコードは、この機能を使用しないコードとABI互換性がありません。

どちらの場合でも、LLVMのターゲットは適切なサイズのアトミックのサポートを主張し、__sync_*関数へのライブラリ呼び出しを介して操作のサブセットを実装できます。AtomicExpandPassで使用される__atomic_*ルーチンとは異なり、これらのルーチンはターゲットLoweringによってネイティブ命令と混在させることができるため、実装でロックを使用しては*いけません*。

さらに、これらのルーチンはステートレスであるため、共有する必要はありません。そのため、1つのバイナリに複数のコピーを含めることに問題はありません。したがって、通常、これらのルーチンは、静的にリンクされたコンパイラランタイムサポートライブラリによって実装されます。

LLVMは、ターゲットISelLoweringコードが対応するATOMIC_CMPXCHGATOMIC_SWAP、またはATOMIC_LOAD_*操作を「展開」に設定し、initSyncLibcalls()への呼び出しを介してこれらのライブラリ関数の可用性を選択した場合、適切な__sync_*ルーチンへの呼び出しを発行します。

LLVMによって呼び出される可能性のある関数の完全なセットは(Nが1、2、4、8、または16の場合)です。

iN __sync_val_compare_and_swap_N(iN *ptr, iN expected, iN desired)
iN __sync_lock_test_and_set_N(iN *ptr, iN val)
iN __sync_fetch_and_add_N(iN *ptr, iN val)
iN __sync_fetch_and_sub_N(iN *ptr, iN val)
iN __sync_fetch_and_and_N(iN *ptr, iN val)
iN __sync_fetch_and_or_N(iN *ptr, iN val)
iN __sync_fetch_and_xor_N(iN *ptr, iN val)
iN __sync_fetch_and_nand_N(iN *ptr, iN val)
iN __sync_fetch_and_max_N(iN *ptr, iN val)
iN __sync_fetch_and_umax_N(iN *ptr, iN val)
iN __sync_fetch_and_min_N(iN *ptr, iN val)
iN __sync_fetch_and_umin_N(iN *ptr, iN val)

このリストには、アトミックロードまたはストアの関数は含まれていません。既知のすべてのアーキテクチャは、アトミックロードとストアを直接サポートしています(通常のロードまたはストアのいずれかの側にフェンスを発行することにより)。

また、これらとはやや独立して、ATOMIC_FENCE__sync_synchronize() に縮退させる可能性があります。これは上記のすべてとは無関係に発生するかどうかが決まり、純粋に setOperationAction(ISD::ATOMIC_FENCE, ...) によって制御されます。

AArch64 では、__sync_* ルーチンの亜種が使用されます。この亜種は、関数名の一部としてメモリ順序を含んでいます。これらのルーチンは、AArch64 Large System Extensions「LSE」命令セットの一部として導入された単一命令アトミック操作が利用可能かどうか、または LL/SC ループにフォールバックする必要があるかどうかを実行時に判断できます。以下のヘルパー関数は、compiler-rt ライブラリと libgcc ライブラリの両方に実装されています(N は 1、2、4、8 のいずれか、M は 1、2、4、8、16 のいずれか、ORDER は 'relax'、'acq'、'rel'、'acq_rel' のいずれかです)。

iM __aarch64_casM_ORDER(iM expected, iM desired, iM *ptr)
iN __aarch64_swpN_ORDER(iN val, iN *ptr)
iN __aarch64_ldaddN_ORDER(iN val, iN *ptr)
iN __aarch64_ldclrN_ORDER(iN val, iN *ptr)
iN __aarch64_ldeorN_ORDER(iN val, iN *ptr)
iN __aarch64_ldsetN_ORDER(iN val, iN *ptr)

AArch64 ターゲットに対して LSE 命令セットが指定されている場合、アウトオブラインのアトミック呼び出しは生成されず、単一命令アトミック操作が代わりに使用されることに注意してください。