C++0x Memory Model 第0回 - メモリモデルとは何か

私は,スカートを履いた女の子に,スカートを履いたままぱんつを脱いで,脱ぎ終わったら右手を挙げるようにと,そう命令した.私は,その子が右手を挙げたのを確かに見た.だが,その子のスカートをめくってみたらぱんつはまだそこにあったのだ!

これは C++ advent calendar の参加記事です。

本ブログエントリは,以降のブログエントリと合わせて, C++0x のメモリモデルに関する規則を具体的な例や意味付けを交えた形で説明していくことを目的としています.

1998年に制定され,2003年に改訂された現行の C++ プログラミング言語標準規格 (以下, C++03) においては,プログラム中にただ1つの実行スレッドしか存在しない場合の規定しか記述されていませんでした*1.しかし, C++ プログラミング言語の次期標準規格 (以下, C++0x) においては,プログラム中に複数の 実行スレッド thread of execution が存在することが許されるようになりました.複数の実行スレッドが存在することが許されるようになることで,プログラム中にただ1つの実行スレッドしか存在しない場合には問題とならなかった細部が根本的な問題を生じるようになります.特に,メモリに対する読み書きに関する様々な問題は,メモリに対する読み書きがプログラムの実行における主要な操作であるだけに,ただちに大問題へと進展します.プログラム中にただ1つの実行スレッドしか存在しない場合しか考慮していなかった C++03 におけるメモリに対する読み書きの規則は,そのままでは複数の実行スレッドが存在する場合にただちに深刻な問題を露呈することになります.そこで, C++0x ではメモリに対する読み書きに関する規則が徹底的に見直され修正されることとなりました.本ブログエントリでは,この C++0x におけるメモリに対する読み書きの規則,すなわち メモリモデル memory model について説明していきます*2

まず,メモリモデルという言葉について説明しておきます. C++0x におけるメモリモデルとは, C++0x プログラミング言語の実装における,メモリに対する読み書きに関する抽象モデル,およびその構成要素としての規則の集合,と定義することにします.ここで C++0x プログラミング言語の実装 implementations of C++0x programming language とは, C++0x の範疇で書かれたプログラムコードを,特定のあるコンパイラ実装がどのような機械語の列に翻訳するか,およびその翻訳結果である機械語の列が特定のあるアーキテクチャ上でどのように実行されるか,を指す言葉とします.したがって, C++0x におけるメモリモデルは,各々のコンパイラ実装が C++0x の範疇で書かれたプログラムコードをどのような機械語の列に翻訳するか,およびその翻訳結果である機械語の列が各アーキテクチャ上でどのように実行されるか,この2つの局面においてある種の制限を各々課すことになります*3

ただ1つの実行スレッドしか考慮しない場合には,メモリモデルに関する細かい規定はほとんど意識する必要がありません.プログラム中に単一の実行スレッドしか存在しなかった C++03 でメモリモデルというものを意識しておられた方は恐らくそれほど多くないでしょう.例として次のプログラムについて考えてみましょう.

int a = 0;
bool b = false;

int main()
{
  a = 1;
  b = true;
  if (b) {
    // b == true ならば a == 1 が成り立つ
    int x = a;
    assert(x == 1);
  }
}

上のプログラムを実行した場合,プログラム中の assert 式は必ず成功します.この assert 式が失敗する可能性を危惧された経験のある方はあまり居ないかと思われます.しかし,今,あえてこれを危惧してみましょう.たとえば, a = 1if 文より後に実行されることは無いのでしょうか? もし仮に, a = 1if 文より後に実行されると仮定すると,当然,上のプログラム中の assert 式は失敗します.

これはただの杞憂でしょうか? 実際,プログラム中にただ1つの実行スレッドしか存在しない場合にはこれは杞憂でしかありません.しかし,なぜこれが杞憂でしかないのかを,直観からではなく,客観的でより正確な形で把握しておくことは重要です.特に,プログラム中に複数の実行スレッドが存在する場合には,先と非常に似たような状況であったとしても,杞憂が杞憂でなくなります.次のプログラムについて考えてみます.まず,先のプログラム中の大域変数と同等の大域変数 a, b が静的に初期化されるとします.

// 静的に初期化される大域変数定義
std::atomic<int> a(0);                       // int a = 0;
std::atomic<bool> b(false);                  // bool b = false;

ただの int, bool 変数だと複数の実行スレッドから同時に読み書きすると未定義動作を引き起こしますので, アトミック atomic な int, bool 変数に変更しておきます*4.この状況の下で,ある実行スレッド T1 が次のコード片を実行するとしましょう.

// 実行スレッド T1 が実行するコード片
                                             //           私は,スカートを履いた女の子に,
a.store(1, std::memory_order_relaxed);       // a = 1;    スカートを履いたままぱんつを脱いで,
b.store(true, std::memory_order_relaxed);    // b = true; 脱ぎ終わったら右手を挙げるように
                                             //           と,そう命令した.

store は値の書き込みを行うメンバ関数です. store メンバ関数の第2引数にある std::memory_order_relaxed についての詳しい説明はずっと後回しになりますが,おおよそ,「アトミックな操作である,以外のいかなる追加の効果も持たせない」という指定だと理解しておいてください.また, T1 と異なる別のある実行スレッド T2 が次のコード片を実行するとしましょう.

// 実行スレッド T2 が実行するコード片
if (b.load(std::memory_order_relaxed)) {     // if (b) {
                                             //                   私は,その子が右手を挙げたのを確かに見た.
  int x = a.load(std::memory_order_relaxed); //   int x = a;      だが,その子のスカートをめくってみたら
  assert(x == 1);                            //   assert(x == 1); ぱんつはまだそこにあったのだ! (この assert は失敗しうる)
}

load は値の読み込みを行うメンバ関数で,その引数の std::memory_order_relaxed については先に説明した通りです.実行スレッド T2 が実行する assert 式が失敗することがある,というのが C++0x のメモリモデルからの帰結となります. T1 が実行するプログラムコードから「b == true ならば a == 1」が明らかに成り立つように思われるにも関わらず,実際にはこれは必ずしも成り立ちません.

上の例から,メモリモデルを強く意識する理由が生じてくることがお分かりいただけるかと思います.プログラム中に単一の実行スレッドしか存在しなかった場合には,プログラマは暗黙にメモリの読み書きに関する様々な仮定を置いており,それは特に意識しなくても成り立つものでした.上の,プログラム中にただ1つの実行スレッドしか存在しない場合の例で言えば,例えば「b == true ならば a == 1」という仮定はプログラムコード中の記述の順序から明らかで,ほとんどのプログラマはこの仮定の成立を疑わないでしょう.一方で,プログラム中に複数の実行スレッドが存在する場合には,このような暗黙の仮定の多くが,そしてそのような仮定に基づいたプログラムのロジック全体が簡単に瓦解しえます.したがって,特にプログラム中に複数の実行スレッドが存在する場合に,メモリモデル,つまりメモリの読み書きについてどのような仮定が成り立ってどのような仮定が成り立たないのか,を強く意識することは大変重要になってきます.

一方,メモリモデルを意識するだけでは事態は進展しません.上の例から分かるとおり,プログラムのロジックが成立するために根本的に重要な仮定の多くは,プログラム中にただ1つの実行スレッドしか存在しないときには成り立っていたとしても,複数の実行スレッドが存在するときには成り立つとは限りません.プログラムのロジックが正しくあるためには,メモリの読み書きに関する様々な仮定が必要になります.それらの仮定が成立するようプログラムを制御するにはどうすれば良いのかも知る必要が出てきます.

C++0x におけるメモリモデルは,一義的にはメモリの読み書きに対する抽象的な規則の集合です.抽象的な規則の集合であることにより,特定のコンパイラアーキテクチャの動作の具体を離れて,汎用で客観的な理解と運用が可能になる利点があります.しかし一方で,無味乾燥で抽象的な規則の集合だけを羅列してもメモリモデルに対する理解を深めることには困難かとも思われます.そこで,本ブログエントリに続くブログエントリにおける説明では,メモリモデルを構成する抽象的な規則群の客観的な説明と並行して,それらがコンパイラの翻訳やアーキテクチャ上の機械語の実行においてどのような意味付けと役割を果たすのか,また,具体的な例で規則群がどのように適用されていくのか,そういった具体的な説明も交えていくことで読者の理解を促していきたいと思います.

それでは, C++0x のメモリモデルについて順次説明していくことにします.

次回の記事: C++0x Memory Model 第1回 - 1.9 Program execution

*1:C++03 ではそもそも実行スレッドという言葉自体が定義されていませんでした.

*2:ある程度普及したプログラミング言語において厳格なメモリモデルを制定することに関しては Java プログラミング言語の規格とコミュニティの経験が先行しています. Java のメモリモデルに関する情報ハブサイトからたどれる情報,特に Java において厳格なメモリモデルが創設される契機となった Double-Checked Locking Idiom に関する議論などは, C++0x のメモリモデルを理解する上で大変大きな助けになるかと思われます.書籍としては, Java に詳しくない私が知っている限りでは "Java Concurrency in Practice" が非常にお勧めできます.

*3:C++03 および C++0x においては, "memory model" という言葉は,オブジェクトの一意性と各オブジェクトのアドレスの一意性に関する規則,また異なるスレッドから同時に読み書きする際にそれらアドレスの一意性がどのように影響するか,を定めた規則を指す言葉です (1.8) .しかし,本ブログエントリおよび後続のブログエントリでは一貫して本文で定義する意味でメモリモデルという言葉を用いることにします.なお, C++03 および C++0x で定義される "memory model" は本文で定義する意味でのメモリモデルに包含されるものとなります.

*4:「同時に」「読み」「書き」「アトミック」などの言葉を説明なしに使いました. C++0x に定義された概念や用語を用いて,これらに対してより厳密に定義された意味を持たせることができます.しかし,今は説明の便宜上,その詳細はずっと後回しにさせてもらいます

任意の expression に透過的に hook を仕掛けるようなマクロが書けるのではありませぬか?

以下のコードでやっていることの説明.

EXCEPTION_HOOK((expr))

と書いておけば, (expr) という C++ の式を実行したら急に例外が来たので……というときに, call stack のようなもので殴られた跡 (この hook が仕掛けられている場所に関する情報) を boost::exception に乗せていきます. call stack の生成ぐらい言語でサポートして欲しいよね,うんうん.終わり. Microsoft Visual C++ 2010 と GCC 4.5.0 で動作を確認済みです.

工夫として,

EXCEPTION_HOOK((expr))

という文字列が式 expr と同等な C++expression として透過的に機能するように見せかけてあります.つまり,たとえば

std::string s("42");
int i = EXCEPTION_HOOK((boost:lexical_cast<int>(s)));

などと書けるようにしてあり,もちろん例外が通知されなければこのコードは

std::string s("42");
int i = boost::lexical_cast<int>(s);

と同じ挙動を示します.終わり.

透過性という点で重要な別の特徴として, preprocessing-time でこの hook を完全に無効化できます.無効化して,字句レベルで (expr) という構文として振舞わせるのは赤子の手をひねる (それをするなんてとんでもない!) よりも造作のないことよ,クックック…….

以下のコードの要点 ([&]lambda-introducer を伴う lambda-expression を用いて, hook を仕掛けた場所のスコープの文脈を持ち込みつつコールバック化する点) さえ抑えておけば,任意の expression に対して,透過的に preprocessing-time, compile-time, runtime レベルの pre-hook, post-hook および例外送出時 hook を仕掛けるようなマクロ (「えーマジマクロ?」「マクロが許されるのは……」) を構築できるんじゃないんでしょうか,知らんけど?

細かいこと言うと hook を仕掛ける対象となる式の型が void でない場合は MoveConstructible でないといけないという制約がががが.ただし,過去,似たようなものはいくつか提示されていますが (Bjarne が昔書いていた operator-> で hook を書く記事どこいったっぽ? *1 ) ,「hook を仕掛ける構文がそのまま expression として透過的に扱える」かつ「例外送出イベントにも hook を仕掛けられる」というのを C++ で達成したものはなかったはずなのでにょほほほほ.

以下,とても長〜いコードと出力例.

*1:追記:あった. リンク先 PDF 注意 http://www2.research.att.com/~bs/wrapper.pdf

続きを読む

GCC 4.5 の半分は C++03 へのやさしさでできています

When printing the name of a class template specialization, G++ will now omit any template arguments which match the default template argument for that parameter. This behavior (and the pretty-printing of function template specializations as template signature and arguments) can be disabled with the -fno-pretty-templates option.

http://gcc.gnu.org/gcc-4.5/changes.html

この文章を読んで衝撃を受ける奴はよく訓練された C++er だ.ホント, C++ は地獄だぜ! フゥハハハーハァー!

#include <string>
#include <map>

int main()
{
  std::map<std::string, std::string> m = 1;
}

GCC 4.5 で -fno-pretty-templates オプションを付けたときのコンパイルエラー. (従来の g++ と同じ挙動)

test.cpp: In function ‘int main()’:
test.cpp:6:42: error: conversion from ‘int’ to non-scalar type ‘std::map, std::allocator >, std::basic_string, std::allocator >, std::less, std::allocator > >, std::allocator, std::allocator >, std::basic_string, std::allocator > > > >’ requested

GCC 4.5 のデフォルトのコンパイルエラー.

test.cpp: In function ‘int main()’:
test.cpp:6:42: error: conversion from ‘int’ to non-scalar type ‘std::map, std::basic_string >’ requested

A*

http://okajima.air-nifty.com/b/2010/01/post-abc6.html
余計なことして遊んでたらマジで制限時間ギリギリの3時間かかってしまった...orz
しかも色々はしょっているわ,出力に S, G が出てないわ...
A* で discover されなかったノードに対する情報を出来る限りメモリに乗せないように (implicit graph っぽく) 作るにはどうするんでしょうねぇ,ということを試したかったので意味不明な書き方になってるんだけれど,まー,この入力で A* (@壁をぶち抜く heuristics) だと結局ノードを全部 discover しちゃってる (ホンマかそれ?) よね...
書いた結果の感想としては,こういう問題 (2次元の迷路) だと A* でやってもあんまり楽しくないので普通に BFS で距離埋めていけば良いのではないでしょうか的な. A* は,あれですよ,ほら.もっとこう本質的に高次元で2つ隣を列挙するのすらやばげな問題のほうが面白いと思う.知らんけど?
GCC 4.5 20100107 snapshot の -std=c++0x オプション付 + Boost 1.40.0 で動作確認っていうか,この C++0x をふんだんに取り入れた贅沢な一品を動かせる実装は恐らく GCC 4.5 以外にまだ無いはず.

続きを読む

C++0x ad-hoc 会議行てきた.\(^o^)/

会議本体のほうの内容は誰か書いてくれるだろう的な他力本願で.以下は完全に個人的な内容.

昼の休憩になった途端,真っ先に名刺くれる方がいらっしゃって,名刺見たらサイボウズ・ラボの光成滋生って書いてあって,そのときの私はたぶん相手の光成さんから見たらすげー反応薄かったように映っただろうなぁと自分で思うのだけれど,これ本当に自分の悪い癖で,表層としては平静を取り繕いつつ,頭の中では「わ,わわわ.で,でもさいぼうずのみつなりさんって,あのさいぼうずのあのみつなりさんですよね?っていうかトートロジーですね.ど,どうしよう.つい先日,友人と話しているときに,友人が目を輝かせながら『すげープログラマが云々』って話してた方がまさにこの光成さんではないでしょうか的な.俺も名前だけは知っているけれどっていうか名前知っている云々じゃなくて,今まさに目の前に居るんですけれど.ど,どうしましょう.話しかけたいけれど,な,ななな,なんて切り出そう?『午後のこ〜だの方ですよねっ!?お世話になりましたっ!』とか,ど,どうでしょう?いや,俺,午後のこ〜だにお世話になりましたでしょうか?しばらくはこ〜だを使っていましたが,その後は lameコマンドラインをいじくりたおす感じになっちゃった気がしますけれどどうでしょうか?いや,ま,まぁ,ここは話のきっかけ的に午後のこ〜だにはお世話になったという結論でいいんじゃないでしょうか?っていうか,人違いだったら大変失礼なことだよなぁ.サイボウズ・ラボって実は他に光成さんって名前の方がいらっしゃったりしませんでしょうか?ど,どどど,どうなんでしょう?」みたいに素でテンパッてて,っていうか後で光成さんのブログ見たら「念願の」みたいな接頭辞が自分みたいな超てけとーな人間に冠されていてギャー!といいますか,そんな風に思われていたとは名刺もらったときには思ってもいなくて,昼飯のときもなんか話しかけるきっかけが掴めず,会議終わった後,こともあろうに一度光成さんを lost しかけたけれど,なんとか追いついて晩飯一緒に食べる流れに持っていけて良かった.っていうかあのまんま光成さんを lost してたら俺一生後悔していたのではないでしょうか的な.「人間は,行動した後悔より,行動しなかった後悔の方が深く残る」今更ながら凄く良い言葉です,ギロビッチ博士っ!

結論:会議や勉強会で凄い人に会ったときに,怖気づいたり戸惑っていたりしたらその分だけ損.

よくよく考えれば,というかよくよく考えなくても,他にもしゃべっとかないといけない方々がたくさんいらっしゃったのに,なんか 0x のドラフトにコメントするぞー的な準備しかしていなくて,もうちょっとお前,こんな方々がいらっしゃるのでとっ捕まえてこんなこと問い詰めるぞー項目を整理しとけよなー,みたいな.

結論;凄い人リストと,凄い人にこんなこと問い詰めるぞーリスト整理しとけ

出水さんと shelarcy さんと光成さんと計4人で晩飯食いつつ結局4・5時間ぐらいしゃべっていたのではないでしょうか的な.

shelarcy さんの話も出水さんの話も面白くなかったわけでは全然ないのですが,自分が今一番興味持っているところでちゃんと実問題と闘って経験値積んでいらっしゃる光成さんのお話がどうしても一番面白かったわけで,なんていうか4時間とかそんなものじゃ全然時間少なすぎる.絶対,時間があればあるほど面白いお話が聞けたはず.次お会いできる機会があったらもっかい問い詰める.

衝撃だったのが Windowsポインティングデバイス皆無で操作していたことで, Visual Studio の起動もコマンドラインからやっているし,窓の移動もキーボードでやってるしで,あれは俺も絶対マスターしようと思った.あれ見てたら Alt+Tab 程度で満足していた俺ぷぎゃー.

自分も何とか面白い話題を提供シナケレバーシナケレバーと, swap と ADL (と scoped concept map) の話を振ってみたのだけれど,よくよく考えてみればこの話はブログでまとめて書こう書こうと思っていながら結局書いていないのであって,要するに人にこの説明をするのが初めてで全然自分の中でもまとまっていない話をしてたことに……orz. C++ 特有の小さい観点じゃなくて,もっと大きな観点から入れば良かった…….

move について光成さんから質問いただいたので,あわわあわわしながら説明してみたけれどなんつーか俺相変わらず説明下手すぎるorz.

あー,当然ですけれど VC の swap は死んじゃえー(^o^)ノ

1ユーザとして PC 使っていて,その使っている PC に100個ぐらいコアが乗っていたらぶっちゃけ何に使おうと思うのかとか,今後の core が many な時代を見据えた種々雑多の話もだいぶできて,しかも話している相手がちゃんと実用レベルでマルチコア相手に泥臭いところにもぐって仕事している経験と実績に裏打ちされた方なので話が面白い面白い.泥臭い部分を押さえると,ちゃんとアプリケーションレベルで有意なパフォーマンス差が出る的な話を聞くことができたのが自分にとって特にプラスで,自分が少しずつ稚拙ながらに進もうとしている方向性というか,パタヘネ・ヘネパタから一歩ずつアーキテクチャやメモリ階層・ I/O なんかを抑えていって,泥臭い部分をしっかり把握した上で並列プログラミングに対する足場をしっかり固めるっちぅ方向性が,少なくとも今後しばらくは意味を持つということが,話していただいた中ではっきり見えてきてとてもありがたかった.

文字列やら locale についてもかなり話し込んだけれど,これは話が進むたびに宿題が増えただけで何も解決した部分は無い気がするのだけれど,ただ普段自分が不満に思っている部分に関しては,やはり最前線の人たちも同じような感覚を共有している部分が多いのだなぁという雰囲気は掴み取れた.これは収穫っと.

後,勉強になったことは,この年齢になってくると往復とも夜行バスによる強行行程は厳しい,と.次の日,丸一日家の布団で死んでた.

最後に,俺,名刺と名刺入れ作っておかないと素でやヴぁいっつーか恥ずかしいだろjk

ぷろあくた\(^o^)/ ぱ〜とつぅ〜

Boost.Asio はもうちょっと↓みたいに proactor な部分で遊ぶのを前面に押し出してほしい気がします的なっていうかこれさっきゆった.
要 Boost 1.36.0 以上.少なくとも Windows Vista 64bit (ただし WIN32 の configuration でビルド) + MSVC 8.0 では動いたような気がする.改行をコンソールに入力するとプログラムが終了するような気もしますっていうかこれもさっきゆった.

続きを読む