Skip to content

Instantly share code, notes, and snippets.

@yohhoy
Last active October 15, 2026 09:56
Show Gist options
  • Select an option

  • Save yohhoy/29475027cf068eea700e2e2c0642046a to your computer and use it in GitHub Desktop.

Select an option

Save yohhoy/29475027cf068eea700e2e2c0642046a to your computer and use it in GitHub Desktop.

C++メモリモデルの変遷(C++11〜C++26)

調査日 : 2026年10月15日

調査モデル : Opus 5.5

C++のメモリモデルは、2007年に採択されたN2429を骨格として、C++11で初めて規格化された。その後の大きな変更は3つある。

  • C++14 : OOTAの禁止規定が、「避けるべきである」という勧告に弱められた(N3786)。
  • C++20 : seq_cstとフェンスの意味論が修復され(P0668R5)、リリースシーケンスが弱められた(P0982R1)。
  • C++26 : consumeが非推奨になり、依存順序の定義が削除された(P3475R2)。

OOTAには現在も規範的な禁止規定がない。C++29向けには、OOTAが起きない理由を説明する非規範注記を加えるP3692R4が採択された。

用語と表記

用語 意味
seq_cst、acquire、release、relaxed、consume memory_orderの各値(memory_order::seq_cstなど)の略記
sequenced-before(sb) 同じスレッド内の評価の順序
synchronizes-with(sw) releaseストアとそれを読むacquireロードの組などで成立する、スレッド間の同期
happens-before(hb) sequenced-beforeとsynchronizes-withを推移的につないだ順序
変更順序(mo) 1つのアトミック変数への書き込みが並ぶ全順序(modification order)
coherence-ordered-before 1つのアトミック変数への操作の間の順序。「書き込み→それを読む操作」「変更順序で前の書き込み→後の書き込み」「読み込み→変更順序でその読んだ値より後にある書き込み」を推移的につないだもの。C++20で導入
全順序S すべてのseq_cst操作が並ぶ単一の全順序。seq_cstフェンスも含む(C++20で明記)
RMW操作 読み込みと書き込みを不可分に行う操作(read-modify-write。fetch_addなど)
リリースシーケンス releaseストアを起点とする書き込みの並び。acquireロードがこの中のどれを読んでも、起点のreleaseストアと同期する
DRF-SC データ競合のないプログラムは逐次一貫性を持つ、という保証
OOTA out-of-thin-air。値が自身の計算に循環的に依存して「無から」生じる実行結果

年表

規格 主な変更 主な文書
C++03以前 スレッドの概念なし Boehm「Threads Cannot Be Implemented As a Library」(2005)
C++11(2011年9月) メモリロケーション、データ競合による未定義動作、happens-before、synchronizes-with、DRF-SCを導入。memory_orderは6値 N2429、N2427、N2664、N3196
C++14 OOTAの禁止規定(29.3p9-11)を勧告に置き換え N3786
C++17 初期化と終了の順序づけのためにstrongly happens-beforeを定義。consumeを使わないよう勧める非規範注記を追加 P0250R3、P0371R1
C++20 全順序Sとフェンスの意味論を修復。strongly happens-beforeを再定義し、旧定義をsimply happens-beforeに改名。coherence-ordered-beforeを導入。リリースシーケンスをRMW操作のみに限定 P0668R5、P0982R1
C++23 モデル本体の変更なし —
C++26 consumeを非推奨にしてacquireと同じ意味に。依存順序の定義と[[carries_dependency]]を削除 P3475R2
C++29(作業中) OOTAに関する非規範注記を追加 P3692R4

C++11 : N2429による骨格

C++03までの規格にはスレッドの記述がなく、マルチスレッドの挙動はpthreadsやコンパイラの慣習に委ねられていた。Boehmは2005年のPLDI論文で、この状態では正しいスレッド実装を書けないと論じた。これが2005〜2007年のWG21の取り組みの出発点になった (Boehm C++MMページ)。

NelsonとBoehmによる N2429(2007-10-05)は、メモリロケーション、変更順序、リリースシーケンス、synchronizes-with、データ競合による未定義動作、DRF-SCを定義した (N2429)。N2429は作業文書に採択され、ドラフトN2462の1.10節に入った (N2480)。

consumeを支える依存順序の仕組み(carries a dependency、dependency-ordered before、kill_dependency)は、McKenney、Boehm、CrowlのN2664(2008)が加えた (N2664)。2010年のN3196で最終調整が入り (N3196)、C++11は2011年9月に出版された。

C++14・C++17 : OOTA規定の後退と初期化順序の定義

C++11のatomics.orderには、relaxedなアトミック操作がOOTA値をストアすることを禁じる規定があった。Boehmの N3786(2013-09-24)は、この規定が長く適用しにくいとして削除した。代わりに、「自身の計算に循環的に依存するOOTA値を生じさせないようにすべきである」という勧告を置いた。厳密な定義はC++17以降に先送りされた (N3786)。cppreferenceがOOTAの記述を「C++14以降」としているのは、この変更に対応する (cppreference)。

C++17では、BoehmのP0250R3が strongly happens-before を定義した。sequenced-beforeとsynchronizes-withを推移的につないだ関係で、consumeに由来する順序を含まない点だけがhappens-beforeと異なる。静的・動的初期化と終了処理の順序づけに使うための定義である (P0250R3)。この用語はC++20で定義し直される(次節)。

このほかC++17では、consumeの代わりにacquireを使うよう勧める非規範注記(P0371R1)が加わった(「consume : 導入から非推奨まで」の節を参照)。

C++20 : seq_cstとリリースシーケンスの修復

何が問題だったか

Boehm、Giroux、Vafeiadisの P0668R5(2018-11-09)は、C++11のseq_cstに2つの欠陥を挙げた (P0668R5)。

  1. 同じ変数にacquire/releaseとseq_cstを混ぜると、Power向けの標準的なコンパイル方式が、規格の禁じる結果を生む。
  2. seq_cstフェンスが弱すぎる。relaxedなアクセスの間にseq_cstフェンスを挟んでも、逐次一貫性が回復しない。

この修正の背景には学術研究がある。Lahavらは2017年、Power向けの標準的なコンパイル方式が、C11のseq_cstの意味論に照らして不健全であることを示し、修復案RC11を提案した (PLV scfix)。P0668の修正は、RC11と同じ方向を向いている(本レポートの解釈)。

全順序Sを、strongly happens-beforeと整合させる

旧規定では、全順序Sはhappens-before全体と整合する必要があった。新規定でSが整合すべき相手は、より狭い strongly happens-before になった。この用語はC++17でP0250R3が定義したものである。P0668R5はその旧定義を simply happens-before に改名し、strongly happens-beforeには新しい定義を与えた。現行ドラフトは、Sがhappens-beforeと整合しなくてもよいことを、注記で明言している (eel.is atomics.order)。2つの関係の違いは次のとおりである。

関係 含むもの 規格での用途
happens-before sequenced-before、synchronizes-with、およびその推移閉包 データ競合の判定、値の可視性
strongly happens-before sequenced-before、両端がseq_cstの synchronizes-with、前後をsequenced-beforeで挟んだhappens-before、およびその推移閉包 全順序Sの制約

出典 : eel.is intro.races

simply happens-beforeは、consumeに由来する順序を除いたhappens-beforeである。C++26でconsumeの依存順序が削除されたため、simply happens-beforeはhappens-beforeと同じものになり、用語としては削除された。

要点は、acquire/releaseの同期だけでつながった辺は、全順序Sの並びを縛らない ことである。以下では、P0668R5が挙げるZ6.Uリトマステストでこれを確かめる。リトマステストとは、ある実行結果が許されるかどうかを問う小さな並行プログラムである。

Z6.Uリトマステスト

ラベル スレッド 操作 読んだ値
W1 1 x.store(1, seq_cst) —
W2 1 y.store(1, release) —
F 2 b = y.fetch_add(1, seq_cst) b = 1
L 2 c = y.load(relaxed) c = 3
W3 3 y.store(3, seq_cst) —
R 3 a = x.load(seq_cst) a = 0

各スレッドの中では、表の上の行が先に実行される(sequenced-before)。問いは、結果「b = 1、c = 3、a = 0」が許されるかどうかである。

図1 : 操作間の関係

flowchart TB
  W1["スレッド1<br/>W1: x = 1 [sc]"]:::t1 -->|sb| W2["スレッド1<br/>W2: y = 1 [rel]"]:::t1
  F["スレッド2<br/>F: y.fetch_add(1) [sc]<br/>1を読み、2を書く"]:::t2 -->|sb| L["スレッド2<br/>L: c = y [rlx]<br/>3を読む"]:::t2
  W3["スレッド3<br/>W3: y = 3 [sc]"]:::t3 -->|sb| R["スレッド3<br/>R: a = x [sc]<br/>0を読む"]:::t3
  W2 ==>|"rf / sw"| F
  F -.->|mo| W3
  W3 -.->|rf| L
  classDef t1 fill:#dbeafe,stroke:#2563eb,color:#111
  classDef t2 fill:#dcfce7,stroke:#16a34a,color:#111
  classDef t3 fill:#fde68a,stroke:#d97706,color:#111
Loading

図1の読み方は次のとおりである。

  • 箱の色 : スレッドを表す(青がスレッド1、緑がスレッド2、黄がスレッド3)。
  • sb : sequenced-before。
  • rf : reads-from。矢印の先の操作が、矢印の元の書き込みの値を読む。
  • sw : synchronizes-with。
  • mo : yの変更順序。Lが3を読むので、FはW3より前に並ぶ。
  • [sc]・[rel]・[rlx] : それぞれseq_cst、release、relaxed。

旧規定では禁止されていた

旧規定では、全順序Sに次の4つの制約がかかる。

  • (1) Rは0を読むので、SでRはW1より前に来る。
  • (2) W1はW2を経由してFにhappens-beforeする(W1 → W2 → F)ので、SでW1はFより前に来る。
  • (3) yの変更順序でFはW3より前にある。Sは変更順序と整合するので、SでもFはW3より前に来る。
  • (4) W3はスレッド3の中でRより前にあるので、SでもW3はRより前に来る。

図2は、これらの制約を矢印で表したものである。図2と図3の矢印 A → B は、「SでAがBより前に来なければならない」ことを表す。

図2 : 旧規定では制約が循環する

flowchart LR
  R["R: a = x"] -->|"(1) Rは0を読む"| W1["W1: x = 1"]
  W1 -->|"(2) happens-before<br/>W1 → W2 → F"| F["F: fetch_add"]
  F -->|"(3) yの変更順序"| W3["W3: y = 3"]
  W3 -->|"(4) スレッド3内の順序"| R
  linkStyle 0,1,2,3 stroke:#d33,stroke-width:2px
Loading

4つの制約はR → W1 → F → W3 → Rと一周するので、すべてを満たすSは存在しない。したがって、この結果は禁止される。

新規定では許される

新規定では(2)が成り立たない。W2はseq_cstではなくreleaseなので、W2からFへのsynchronizes-withはstrongly happens-beforeにならないからである。

図3 : 新規定では循環が解ける

flowchart LR
  R["R: a = x"] -->|"(1)"| W1["W1: x = 1"]
  W1 -.-|"(2) strongly happens-beforeではない<br/>→ Sへの制約なし"| F["F: fetch_add"]
  F -->|"(3)"| W3["W3: y = 3"]
  W3 -->|"(4)"| R
Loading

(2)が外れると制約は一周しなくなり、例えばF → W3 → R → W1という順のSが成り立つ。この順序は(1)(3)(4)をすべて満たす。また、SでRより前にxへの書き込みが無いので、Rは0を読める。したがって、この結果は許される。

上の(1)〜(4)の推論は、論文と現行ドラフトの定義をもとに本レポートが組み立てたもので、論文にこのとおり書かれているわけではない。

規格を弱めた理由と、残る保証

規格を弱めた理由は、ハードウェア上の実装にある。Powerの標準的なコンパイル方式は、releaseストアをlwsync命令にマッピングする。lwsyncには、スレッド1のW1をスレッド3から見えるようにする累積性がないので、実機ではこの結果が起こりうる。これを禁じるには、releaseかseq_cstの実装を高価にするしかない。論文はどちらも退け、規格を実装の実態に合わせた (P0668R5)。

一方で、次の保証は失われていない。

  • seq_cst操作だけを使う、データ競合のないプログラムは、引き続き逐次一貫性を持つ。
  • acquire/releaseの辺も、前後をsequenced-beforeで挟めばstrongly happens-beforeになる。両端の操作のメモリオーダーは問わない。
  • フェンスを介した順序づけは、むしろ強化された。seq_cstフェンスがSに含まれることは、C++11の時点で前提になっていた(LWG2130でBoehmが確認している)。C++20はこれを「including fences」と明記し、フェンスに関する制約を、新設のcoherence-ordered-beforeを使って一般化した (P0668R5)。

代償として、同じ変数でacquire/releaseとseq_cstを混ぜると、直感に反する結果が起こりうる。規格もこの点を注記で警告している。

リリースシーケンスの弱体化

P0982R1(2018-11-06)により、リリースシーケンスは、起点のreleaseストアと、それに続くRMW操作だけで構成されることになった。同じスレッドによるrelaxedストアは含まれなくなった。理由として、そうした使い方の例がほとんど無いことと、無関係なスレッドが加わるだけで同期が壊れうることが挙げられている。参照カウントのような典型的な用法はRMW操作で書かれるので、影響を受けない。この変更は、互換性を壊す変更としてAnnex Cに記載された (P0982R1)。

consume : 導入から非推奨まで

consumeの目的は、RCU(Read-Copy-Update)のようにポインタを公開する処理で、ポインタの指す先のデータに限った順序づけを、PowerやARMで追加のフェンスなしに得ることだった (N2664)。しかし、規格どおりの実装は現れなかった。P0098R1はその理由を3つ挙げている (P0098R1)。

  • 規格上の「依存」が広すぎる。x - xのように最適化で消える式でも、依存の連鎖を保つ必要がある。
  • 関数をまたぐ連鎖には、[[carries_dependency]]やkill_dependencyをあちこちに書く必要がある。
  • 値の投機的実行などの最適化が、連鎖を壊す。

結局、既知のコンパイラはすべてconsumeをacquireとして扱った (P0371R0)。2016年の時点では正式な非推奨化は見送られ、consumeの代わりにacquireを使うよう勧める非規範注記(P0371R1)を入れるにとどめた (P0371R1)。その後の再定義案(P0190R4、P0462R1、P0750R1)は、いずれも採択されなかった。

決着をつけたのは、C++26に採択されたBoehmの P3475R2 である (P3475R2、draft issue #7656)。C++26の変更点は次のとおりである。

  • memory_orderの本体からconsumeを外し、Annex Dで、acquireと同じ意味の別名(値は1)として残した (eel.is depr.atomics.order)。
  • kill_dependencyをAnnex Dに移し、引数をそのまま返す関数にした。
  • carries a dependencyやdependency-ordered beforeなど、依存順序の定義を削除した。
  • [[carries_dependency]]属性を削除した (eel.is dcl.attr.depend)。

P3475が挙げる理由は3つある。コンパイラが実装しないこと、ハードウェアが変化したこと(x86ではconsumeを使っても速くならず、ARMではacquireロードが安価になった)、そして代替案が無いことである (P3475R0)。なお、cppreferenceのC++26機能表は採択版をR1と記載しているが、ドラフトのissueとlibc++の記録が示す採択版はR2である。

[[carries_dependency]]を書いた既存のコードは、C++26でもエラーにならず、単に無視される。根拠は次の2つである。

  • 規格に定めのない属性の扱いは処理系定義であり、処理系が認識しない属性は無視される([dcl.attr.grammar] p8)(eel.is dcl.attr.grammar)。削除されたcarries_dependencyは、この「規格に定めのない属性」に当たる。
  • P3475R2は、この属性を非推奨にする理由は無いとして丸ごと削除し、残った使用箇所は「いずれにせよ無視されるだろう」と述べている (P3475R2)。

P3475R2は同時に、__has_cpp_attributeの表からcarries_dependencyの行(200809L)を削除し、Annex Cの[diff.cpp03.library]にある「マクロ名に使えない属性名」の一覧からもこの名前を外した。

Linuxカーネルは、もともとconsumeを使っていない。READ_ONCE()やrcu_dereference()と、ポインタに限った運用規則によって、依存順序を得ている (rcu_dereference.rst)。

OOTA : 禁止の定義から「起きない理由の説明」へ

現行ドラフトの[atomics.order]にあるOOTAの規定は、C++14でN3786が入れた勧告と、例を示す非規範注記だけである (eel.is atomics.order)。OOTAの禁止を厳密に定義する試みは、何度も行き詰まった。

  • N3710(2013) : 因果関係によって禁止を定義する案。ARMやGPUでの実装負担が大きく、合意に至らなかった (N3710)。
  • P1217R2(2019) : 問題は誰もが認めるが、合意された解は無いと整理した (P1217R2)。
  • P1780R0(2019) : Modular Relaxed Dependenciesと呼ぶ定義を提案したが、採択されなかった (P1780R0)。
  • P2055R0(2020) : OOTAが実際に起きた例は知られていないとした (P2055R0)。

その後、方針は「禁止を定義する」から「実装で起きない理由を説明する」に変わった。P3064R2(2024)は、既存のコンパイラとハードウェアはOOTAを生じさせないので、規格を変える必要は無いと論じた (P3064R2)。これを短くまとめたのが P3692「How to Avoid OOTA Without Really Trying」で、[atomics.order]に非規範注記を2つ加える提案である (P3692R4)。

C++29向けに採択されたのはR4で、cplusplus/draftのPR #9111でドラフトに反映された (issue #2322、draft PR #9111)。現行ドラフトには、すでにこの注記が入っている。

まとめ

C++メモリモデルの改訂は、一貫して、規格が約束したものの実装が実現できなかった部分を後から削る方向に進んできた。

  • OOTA : C++11の禁止規定は、C++14で勧告に後退した。現在は、起きない理由を説明する注記を加える段階にある。
  • seq_cst : Power上の標準的な実装に合わせて、C++20で規格が弱められた。同時に、フェンスの意味論は強められた。
  • consume : 十数年にわたって規格どおりの実装が現れず、C++26で事実上撤回された。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment