shellのfor文について

最近技術系のことをしっかりまとめる時間が減ってきた。とりあえず小粒なものを書いておこうと思う。

2ヶ月前くらいに、AからJまで10個のアルファベットを順に取り出したいことがあった。つまりこういうことがしたい。

grep 'A' something.dat | some_command | ...
grep 'B' something.dat | some_command | ...
grep 'C' something.dat | some_command | ...
# ...
grep 'J' something.dat | some_command | ...

これを行コピーして書き換える者は「怠惰」ではない。 一応、bashには配列があるが、文法が微妙に複雑で、使おうと思った時は大抵忘れてしまっている。結局毎回「bash 配列」などで検索する羽目になる。

declare -a alphabets=("a" "b" "c")

これはわかりにくくないか?

ところで、bashforは意外と柔軟だ。

for a in A B C D E F G H I J; do
    grep ${a} something.dat | some_command | ...
done

これが動く。

まあA B C D E F G H I Jを打つのは面倒なことに変わりはない。個人的には、普段ループに使っているseqで動けば一番いいのだが、と思っていた。 例えばこれは以下のようにすれば何とかなるだろう……と思ったが動かなかった。

for i in `seq 65 74`; do
    grep $(printf "%c" ${i}) something.dat | some_command | ...
done

どうやらこうすると最初の1文字目が出力されるだけでASCIIコードに対応する文字が出るわけではないようだ。 他にはhexにしてから0x4cなどのようにして変換させるという方法があるが、どんどん可読性が落ちていってしまう。

そして最近、以下が動くことを知った。

for a in {A..J}; do
    grep ${a} something.dat | some_command | ...
done

最初の苦労は何だったんだ?

その上、以下のようなことが起きる。

$ for a in {A..C}{1..3}; do
    echo ${a}
done
A1
A2
A3
B1
B2
B3
C1
C2
C3

今までの苦労を返して欲しい。

ところでfishでこれをする方法を探したが、どうもなさそうに見える。かなしい。

Better document differences from bash · Issue #2382 · fish-shell/fish-shell · GitHub

自作プロダクトにIssueが立ったのでManjaroを触った話

背景

私が作った個人プロダクトの中で、特に宣伝していない割にはそれなりに(見ず知らずの人に)スターを貰って使ってもらえているものがあるのだが、そこにIssueが立った。

internal compiler error with gcc-7.3.1 · Issue #9 · ToruNiina/toml11 · GitHub

ちなみにこれはTOMLという設定ファイルのパーサである。

このIssueは、Arch系でリリースされているgcc-7.3.1だけで(本家では7.3.0までしかリリースされていない)生じるInternal Compiler Errorに関するものだ。それまでは出ていなかったICEが非公式リリース7.3.1で突然発生しているのだからコンパイラのバグかと思ったのだが、微妙な処理系依存動作に乗っかっていたのかもしれない。どちらにせよworkaroundできた方がいい。

それで報告されたエラーメッセージを見た所、おそらくSFINAE関係だろうと推測できた。他のライブラリが問題なくコンパイルできているのだとしたら、その箇所でやっていた妙なことといえば、constexpr関数をtemplateのデフォルト引数に使っていたことだ。

constexpr int func() {}

template<typename T, int I = func()>
class X{};

というような感じだ。 おそらくここでおかしくなっているのだろうと何の根拠もなく直感した。

そしてboostなどがコンパイルできるのなら、structを使った普通のメタ関数は普通にコンパイルできるのだろう。そう思ってconstexpr関数を使っているところを無くした。リファクタリングも兼ねてメタ関数を整理もした。とりあえずTravis.CIとappvayorのテストは通った。

そして、予想があっているかどうかを確認するため、VMを立てた。

本題: Manjaro on VirtualBox

Arch系で簡単に使うとなると、Archそのものは厳しいので、Manjaroだろう。使ったことはないが記事で見たことがある。そしてIssueの報告者もManjaro使いだったのでこれ以外の選択肢はないだろう。

というわけでVMを立ち上げてManjaroをインストールした。gccのバージョンを確認する。

[manjaro@manjaro ~]$ g++ --version
g++ (GCC) 7.3.1 20180312

よし、ということで件のプロダクトをcloneしてcmake……と思ったが、cmakeがない。確かArchのパッケージマネージャはpacmanとか言ったな、と思いググってインストールしようとしてみる。

[manjaro@manjaro ~]$ pacman -S cmake

と思ったが何やらエラーが出ている。「database file for "core" does not exist」というようなエラーだ。 よくわからなかったまま十分ほどgoogleし、どうやらManjaroのインストール後は、最初にpacmanのデータベースファイルを更新しなければならないらしいとわかった。

$ pacman -Syy

を実行すると、どうやらどこかのサーバからパッケージのリストのようなものがダウンロードされている感じがして、インストールできるようになった。

こういうので時間を食ってしまうのは仕方がないが少しつらい。

結果

で、Issueはどうなったのかというと、直っていた。workaround成功だ。というわけでArch使いの方も安心して我がプロダクトを使っていただきたい。

GitHub - ToruNiina/toml11: TOML for Modern C++

プログラミングパラダイムとは制約をかけることだという見方

ふと感じたのだが、プログラミングパラダイムというのは、プログラムに制約をかけることなのではないか。

基本的に、現代で書かれるプログラムはどれも非常に長く複雑で、人間の脳みそに収まりきらない。実行パスをトレースできない規模のものはザラにあるし、長さがそもそも覚えられないものもある。

このようなプログラムを頭から完全に理解するのは不可能だ。大規模なプログラムを頭から完全に理解するというのは単に文字列として覚えるという意味ではなく(そもそもそれが不可能だと思うが)、様々な入力や内部状態に応じてどの実行パスを通りどの処理が実行されて、それに伴ってどの程度のメモリが消費され、どのスレッドがどの領域のMutexを取得して……。プログラムを文章として読むだけではなく、頭の中でその挙動をエミュレーションできる必要があり、さらに爆発的に増えていく状態や入力の組み合わせの全てに対して実行してみる必要があるのだ(頭の中で)。そもそもそのようなことをしようとすることが時間の無駄のように思える。だいたい、活発に開発されているプログラムは同時並行で異なる部分が編集されていったりする。すると知ったそばから内容が変わっていたりする。こうなると理解するのはもう不可能だ。

なのにどうやってプログラムは開発されているのだろう。理解できていないものを適当に変更したらたいていの場合壊れるのだが、壊れずに動いているプログラムが多いのは何故だろう。それは、主だったプログラミングパラダイムや積み上げられてきた知見がどれも、組み合わせ爆発を抑えていくようなものになっているからだ、とふと思ったのが今回のこの記事だ。

オブジェクト指向的な考え方が何かというと、大雑把には、単一の仕事に対して責任を持つクラスを作り、それに仕事をさせるためのインターフェースを設計し、そのクラスはその仕事を完璧にこなし、外に内輪の情報は何も漏らさないというものだろう。これは、単一の仕事についてそれをこなすために必要な処理を全てブラックボックスに閉じ込めることで、プログラム全体の状態数を減らすという戦略だと言えなくもない。

関数型はもっとわかりやすく、状態が作られることがないので、その部分の複雑さは一切ない。その瞬間にその関数に渡ってきた入力が全てなので、見るべきものが非常に小さくなっていることになる。

このような観点から見ると上の2つは同じことだ。単一の仕事について、その処理をまとめ、それに付随する内部状態が全体に波及しないようにする。簡単な話じゃあないか。

特定の部分について考えているときに他の部分については考えずに済むなら、頭から爪先まで理解している必要はない。読んでいる部分に集中すればよい。

そう考えてみると、goto-lessプログラミングもそのような見方で捉えられなくもない気がする。というのも、gotoがあるプログラムは今読んでいる行に突然全く別の内部状態で処理が遷移してくるかもしれないと怯えながら読まなければならないので、これはつまりプログラムの全状態の組み合わせを全ての行で考慮しなければならないことを意味する(これは若干誇張しているが)。なので、この強すぎる武器gotoを使わないようにする、または使う場面を非常に強く制限することで、考慮すべき状態数を減らしたのがgoto-lessプログラミングだと言えるだろう。

これらは人間の小さな脳みそが組み合わせ爆発に対抗する手段だ。そういう発想があれば、どのパラダイムも納得がいくようになるのではないか。

古い標準ライブラリ実装を新しいコンパイラで使って困った話

背景

C++11ではconstexprメンバ関数は暗黙にconst指定される。

template<typename T>
class X
{
  public:
    constexpr T get_member() /* const */ {return member_;}
  private:
    T member_;
};

この仕様はある程度面倒を引き起こしていた(参考:http://boleros.hateblo.jp/entry/20130604/1370364968 )。

しかし、C++14ではめでたくこの仕様はなくなった。

事例

さて、絶妙に古いコンパイラが入っているマシンでC++14コードをコンパイルしようとしたのだ。バージョン番号は忘れてしまったが、GCCではC++14対応がなかったものの、clangは対応していたので、clangで普通にコンパイルすれば通るだろうと思った。実際、簡単なコードは通っていた。

しかし、別のコードをコンパイルしようとすると驚くべきことにstd::complex::real()が呼べないという問題が発生する。Clang曰く、「const std::complex& に対して const 指定されていないメンバを呼び出した」という。std::complex::real()はそもそもconstだ。非const版は引数を一つ取る。

これは何かがおかしいと思い標準の実装を見てみた。すると以下のようになっていた。

# if //C++11以上かどうか確認のマクロ
constexpr T real() {return _M_real;}
# else
...

const指定がない。この標準ライブラリ実装はC++14以前のもので、つまりconstexprなので暗黙にconst指定されていたのだ。なので陽にconst指定されてはいなかった。だがclangはC++14に対応しており、私も-std=c++14を渡していたのでこの関数は非constになっていた。よってconst性をviolateしてしまい、呼べなくなっていたのだ。

標準ライブラリ実装を分ければよかったわけだが、そこを横着したための問題ということになる。

結論

コンパイラ野良ビルドし、環境が用意している古いコンパイラとライブラリ実装は全て無視しよう。

追記(4/23)

📰2018-04-22のニュース - ゆなこん Yuna Computer System で取り上げて頂いたのだが、読み返してみるとこの記事がかなり言葉足らずだったことに気づいたので、少し追記しておこうと思う。

この時、何も考えずにコンパイラだけ変更してコンパイルしたので、古いGCC実装の標準ライブラリが新しめのclangによってコンパイルされると言う状況が完成しており、上記のような問題が起きたのだった。

GCCからしてみればC++14以前なので、constexprメンバは暗黙constでよかった。clangからしてみればC++14対応は済んでいたので、constexprメンバは暗黙constではなかった。誰も悪くなかった。悪かったのは何も気を使わずに横着してその2つを組み合わせた私だったと言うわけである。

と言うわけで、野良ビルドしてPATHや使うライブラリ実装に気を使っておけばこう言うことにはならないので、野良ビルドしよう。

プログラミングに挫折していた頃のこと

読者諸兄は、雪が降り積もって行くその過程を眺めていたことがあるだろうか。

雪が降り積もる時、雪は初めから着実に蓄積してゆくのではなく、初めは地面に触れては融け、触れては融けを繰り返す。そうして無為に見える時を過ごした後、地面が十分に冷えた時を境に、そこから初めて雪が積もり始めるのである。


唐突に思い出したのだが、私は3回、プログラミングに挫折している。

最初の一回は、まず条件分岐を理解していなかった。今で言うScratchのような、タイルを敷き詰めることでプログラムを書くような教育用言語を触ったのだが、動いて欲しいと思っている内容をコードに落とせなかった。小学校の頃だったので、そもそも自我がなかったせいではないかと思う。最後の一回は、制御フローはわかってきていたのだが、データ構造という概念がなく、ひたすらに冗長になってしまい嫌になっていた。配列を知らずにint a1, a2, a3 ...; のようにしてしまうやつだ。あと、今なら二分木かハッシュ、または find を使うところで無限に else if を書いたりしていた。そして小規模なプログラムの行数が1000行近くに達し、何もかもがわからなくなるという事態を引き起こしていた。

悪名高いポインタを知ったのは、プログラミングを4回目に始めた時だ。つまり今に繋がっている、そこから一度も投げ出していない(一年以上コードを書かなかった時期を投げ出していたと定義する)一連の流れの最初の頃である。ポインタで詰まった記憶はない。データがメモリにあるというモデルは頭の中に既にあったので、それを指しているもの、と非常にすんなりと理解した。たまに聞く p += sizeof(T) のような引っかかり方もしなかった。というのも当時読んでいた本に、ポインタに1足すと次の要素を見るようになる(sizeof相当の値をアドレスに足す)とexplicitに書いてあったからだ。その後順調にリンクリストや木構造の実装を知り、ポインタの便利さを理解し、のめり込むようになって行った。

さて、では真ん中の一回、挫折したのは何故だったか。あれは中学の頃だったと思う。条件分岐を理解して、非常に簡単なプログラムが書けるようになっていた。すると次にしたくなることはループだろう。確か、自分がやめていいとシグナルを送るまで回り続けるようにしようとして、私は以下のようなコードを書いた。

main:
    // do something...
    call main

これは走らなかった。「ネストが深すぎます」というエラーが出たのだ。当時の私は「ネスト」という言葉を知らなかったので、検索してみた。どうやら入れ子構造を作りすぎているということらしいとはわかったのだが、当時の私はその何が悪いのかわからなかった。

今ならわかる。関数の再帰が深すぎるので、スタックオーバーフローを引き起こす可能性を憂慮し、言語処理系が止めていたのだ。だが当時の私はスタックなどと言うものを知らない。しかも、これはある意味「正しい」コードだ。動くはずだ、と私は思っていたし、動かない理由を思いつけなかった。これはプログラムという抽象的なものが有限の資源しか持っていない現実のマシンで動いていることを意識しないと辿り着けない答えのような気がする。特に、当時の自分は callgoto か何かのように理解していたはずなので、これが動かない理由は真剣にわからなかった。よって回避策も考えつかなかった。私は簡単なプログラムを動かすことにも失敗し、しばらくプログラミングを投げ出していた。

今も、当時あの言語処理系が末尾最適化をするぐらい賢かったらどうなっていただろうと考える。プログラミング経験が4, 5年伸びていたかもしれない。ぴったりハマってから今まででこれだけ知識が増えたことを思うと……いや、起きなかったことを考えても仕方ないのだが……。

どんなことでも、一度ピタッと嵌るまでが難しい。最初は何がおかしいのかもわからないし、自分が何をわかっていないかもわからないからだ。一度ある程度点と点が繋がるところまで来ると、その後は何をわかっていないのかくらいはわかるようになり、爆発的に学習が進む。不思議な話ではあるし、じゃあどうやったら最初に目の粗い網が張れる程度に物事が繋がるところまで行けるのかと言われると、私は忘れてしまったのでよくわからない。

不思議なことに、何かよくわからないままにもがいて、あるいは放置して別のことを勉強しているうちに、戻ってきた時突然すんなりと理解できるようになっていたりする。何故なのかはわからない。関係ないと思っていた分野に実は根底が同じものがあって素地ができていたか、時間経過で昔の知識が整理されて見通しがよくなったか、それとも単に局所最適に囚われて変な見方をしていたせいでわからなくなっていたのが解放されたのか。理由はわからないが、これまでも何度もこう言うことがあり、聞いてみると知人の多くが同じ経験をしていることがわかった。

冬が好きな知人の一人は、この現象を積雪に譬えた。雪が積もる日も、初めは雪は地面に触れると融けてしまう。しばらくそれが無為に続いたのち、ある瞬間、地面が十分に冷えて、それ以降は雪が融けずに積もり始める。無為に見える過程でも、見えないところで変化は進行している。そしてあるところでそれがわっと外に見える形で出て来ることがある。大事なのは、無為に見える間に諦めてしまわないことなのだと。

さて、今日も地面を冷やそう。

バイナリアンに出会った話

飲み会の席で隣に座っていた人が、聞いてみるとゲームのデバッガーをやっているらしい。でも開発プロジェクトの一員という訳ではなくて、コードそのものは見られない立場にいるらしい。なんだそりゃ、と思ったが、そういえばテストプレイヤーの募集を見たことがある。そういうものなのかもしれない、と思ってそのまま話を続けることにした。

ところが彼はただのテストプレイヤーではなかった。彼はバイナリハッカーだったのである。というのも、彼はデバッグするに当たって、愚直に色々な状況を作って試すのではなく、自分でバイナリを解析するようになったらしいのだ。チートをするプレイヤーが出ないように、チートが可能になりそうな箇所を先んじて見つけることもあるという。

彼には酒とその場の勢いで色々なハックを教えてもらった。彼はプレイの最中にそのプロセスが使っているメモリの内容をダンプしたものと、逆アセンブラを主に使っているらしい。

例えば、ゲーム中で戦闘が始まり、自分が敵にダメージ10を与えたとする。するとメモリのどこかの値が10減ったりする。そうすると、メモリ上のどの位置がその時の敵のHPを意味しているかがわかる。これは基本的な戦略だ。メモリ上のどの位置が何を意味しているかわかれば、その分干渉が可能になる。

彼曰く、アマチュアハッカーはその値を直接書き換えるのだという。敵のHPを直接0にしてしまえば、次のフレームでHP判定が入って敵が倒れる。自分のHPを9999にしておけば、死ぬ心配がなくなる。私も知っている基本的なハックだ。だが彼に言わせるとこれは甘いらしく、というのも効果が一回限りだからだという。確かにそうだ。例えば次の戦闘が始まれば別の敵のHPを0にしなければならないし、自分が全回復すれば正常な値に上書きされてしまう。

ではプロのハッカーはどうするのかというと、そのメモリ領域を指すポインタを探すのだという。メモリアクセスも解析できるので、どのコードブロックからアクセスがあるかがわかる。すると、そこを逆アセンブルすれば、概ねどういう処理をしているかがわかる。例えば自分が死んでいないか判定するルーチンがあったなら、そこで自分のHPに即値9999を代入するコードに変えてしまえば、毎ターン自分のHPが9999になるのだという。

そういうことをするためには、書き換えてもバイナリのサイズが変わらないように気を使う必要がある。というのも、バイナリコードの場所は非常に重要なので、1byteでもずれると例えばjmpの先がずれたり、命令がCPUのページ境界を跨いで割り込みが発生したり、全体がクラッシュするからだ。なので、書き換えても構わないコードの存在は、それ自体がセキュリティホールになるのだ、と彼は教えてくれた。

ではどういう部分なら書き換えられるのか。彼によると、デッドコードがその筆頭で、リリースされていない新機能やDLC用の処理などは格好の餌食になるらしい。ゲームなのでそういう部分は存在してしまう。そこを書き換えて自分のコードを埋め込み、また別のどこかを書き換えてそこへ飛ばす。他にも、滅多に呼ばれないルーチン、例えば正常なプレイの範疇では起きないオーバーフローを直すためのルーチンや、特殊なイベントのための処理なども餌食になるらしい。

より面白かったのは、もっと微妙なケースだ。あるレジスタ、例えばeaxに即値0を入れたいとする。素直に書くとmov eax 0になる。だが、このeaxレジスタは4byteなので即値0も4byte整数が埋め込まれる。これはロスになると彼は言う。というのも、xor eax eaxeaxの値はクリアできるからだ。これは確かにバイナリでよく見るオペレーションである。mov eax 0のバイナリコードをxor eax eaxにすると、数バイト浮く。そのような空きを集めてjmpなりcallなりの色々な命令を埋め込んでいくらしい。酒の酔いのせいか、目が回りそうだった。

なので無駄のないサイズの小さなバイナリは、それ自体が結構セキュアになるのだ。ハッカーがコードを埋め込める場所が少ないからだ。バイナリのサイズそのものがセキュアかどうかをある程度表しているというのは予想外だったので、これは意外な発見で、面白いなと思った。

では実際jmpだけを埋め込めるようなケースがそれ単体でも役に立つことがあるかどうかは気になるところだろう。彼曰く、戦闘のルーチンで即脱出できるなら当然これはチートの役に立つ。この話を聞いて当然浮かぶ質問は、メモリリークはないのかということだ。なので質問してみた。すると彼は、その場合は「正しい場所」を探してそのアドレスへ飛ぶのだと言う。つまり、もし戦闘のためのサブルーチンにjzなどがあったなら、そこではおそらく敵のHPが0になったかどうか、つまり死亡判定をしている確率が高く、そこまでジャンプできれば戦闘が正常に終わって勝利した場合のコードまで飛べるのだ。なのでメモリ確保が走っている場合は(これは単に逆アセンブルした内容を追っていけばわかる)、cmpjzなどを探してそこまで飛べば、解放処理もするはずだ。元のコードがメモリリークしていない限りは。

ところで、チートで使われる技法にはメモリ上のどこが何を意味しているか解析できる前提のハックが多い。非常にシンプルだが効果的なチート対策として、コードではなく、変数の値の難読化があるのだという。例えば、HPに定数を足しておく。そして死亡判定処理の時だけその定数を引く。あるいは、HPを負の値にしておいて、表示する時だけ符号を変える。また、整数値でいいところをわざと浮動小数点数で持っておく。こうすると、ビットパターンがチーターの探す値のビットパターンと変わってくるので、メモリダンプの内容からそこを見つけ出すことが難しくなるのだ。逆アセンブリの結果を難読化する手法は聞き覚えがあったが、実際に働いている人から聞くとまた格別に面白い。ずっとコードを難読化するのだろうと思っていたのだが、思いの外シンプルな方法で、しかし劇的な効果があるのだという。

彼は慣れているので、バイナリのどのあたりがデータセグメントでどのあたりがコードセグメントなのか16進ダンプしただけでなんとなく見当がつくのだという。多くのゲームで使われる定数はそんなに大きな値ではない(ゲームによるがせいぜい10000程度)ので、非常に桁が大きいバイナリが続いているところはデータセグメントではなさそうに見えるらしい。ただ、テクスチャや音楽がコードされている部分はこの方法では見分けられず、コードセグメントだと勘違いしてクラッシュさせてしまったことがあると彼は語っていた。

データセグメントが見つかると、その中に定数、例えば敵のHPの上限などが入っていたりすることがある。その場合、その定数を0にしてしまうと、敵が出てきた次のフレームでは勝利するようになる。そういった書き換えも立派なチートだ。

しかし、彼はコンパイラの最適化などが謎のマジックナンバーを生成することがあるなどの話は知らなかった。私がいくつかの事例や、そもそも人間が謎のビット演算によって一部の重い処理を置き換える話などを話題に挙げたところ、彼はそのほとんどを知らなかった。彼はバイナリを解読する人間であって、コンパイラが行う最適化には詳しくないらしい。それはそれで不思議な話だ。私のこれまでの知り合いにはいなかったパターンなので、非常に話が弾んだ。彼も職場ではあまり低レイヤーの話で盛り上がることは少ないらしく(これも私にとっては非常に疑問だったが、どう言う会社なのだ?)、私との会話を楽しんでくれているように見えた。久しぶりにほぼ初対面の人間と楽しい会話ができた気がする。

ただし思い出して欲しい。最初に言った通り、私はその時飲み会に参加していたのだ。もちろん、レジスタの名前やニーモニックが飛び交う私と彼の会話が非常に弾んでいる横で、他の人々が我々を奇異な目で見ていたことは、わざわざ説明する必要もないだろう。


ところで『楽しいバイナリの歩き方』という本があり、この手のことが少し平易に解説されている。一年くらい前に読んだが、低レイヤーのことに興味を持ついいきっかけになる本だと思う。他に、『低レベルプログラミング』と言う本を今読んでいる。これは結構ガチな本で、少し時間をかけて読んでいる。『BINARY HACKS』はだいぶ前にパラパラ見て、一度読むのを諦めて塩漬けにしてしまったが、今なら楽しめそうな気がしてきた。『Hacking: 美しき策謀』は友人が持っていて、色々と勧めてくれたが、まだ読む機会を得ていない。当時の私には難しすぎた。あと次に本屋に行く機会があれば『ハッカーのたのしみ』を買おうと思っている。前に近くの中規模の本屋に行った時には置いていなかったので。

C++17: 動的メモリ確保とアライメント

背景

アライメントまわりのことを調べていたらC++17でaligned_allocnewの新しいオーバーロードが入っていたようで、少し規格書(N4659)と元になったP0035R4にあたってみることにした。

(最初C++2aのN4727を見て書いていたのでN4659を確認したが、内容に特に変化はない)

アライメントとは

ほとんどのアーキテクチャは、任意の位置からメモリの内容を読み込むことができない。これはアクセスできないメモリ領域があるということではなくて、メモリから取ってこれるデータの位置とサイズには限界があるということだ。例えば、一回のメモリアクセスでは16バイト分のデータを16の倍数のアドレスからしか読み込めない、という制限がある。それがユーザーのコードにどう影響するかはアーキテクチャによって異なるわけだが、一つの値を一回のメモリアクセスで取ってこれなかった場合、最善でもオーバーヘッドが生じる。

例えば、中身が{x, y, z, w}になっている8バイトのデータがあり、それをメモリから取ってきたいとする。もしこれがアクセス境界に並んでいなかった場合、アクセスは複数回生じるだろう(アーキテクチャに依るが、割り込みが発生する場合もある)。

一回目のアクセス
 v v v v
| | |x|y|z|w| | |
         ^ ^ ^ ^
    二回目のアクセス

だがこれをメモリアクセスの境界にちゃんと並べられたなら、アクセスは一回で済む。

一回目のアクセス
 v v v v
|x|y|z|w| | | | |

構造体の中にパディングが入る理由は、コンパイラがメモリを多少無駄遣いしても、構造体のメンバ、または構造体そのものへのアクセスを最適化しようと頑張るからだ。逆にメモリを全く無駄遣いできないときは、パディングを入れないようにコンパイラに伝えるか、最小限で済むようにメンバの配置を考える必要がある。

通常より大きなアライメントを指定したい状況

よくある例を挙げよう。多くのCPUがSIMD命令を持っている。これは複数の値に対して同じ命令を同時に適用できるというものだ。例えばIntel AVXは256bitの演算幅を提供しており、32bitのfloatなら8つ、doubleなら4つまでを同時に計算できる。これはイントリンシック関数を使うと

#include <immintrin.h>

__m256d v1 = _mm256_set_pd(3.0, 2.0, 1.0, 0.0);
__m256d v2 = _mm256_set1_pd(0.5);
__m256d v3 = _mm256_add_pd(v1, v2);

std::array<double, 4> result;
_mm256_storeu_pd(result.data(), v3);

// {0.5, 1.5, 2.5, 3.5}
std::cout << '{' << result[0] << ", " << result[1] << ", "
                 << result[2] << ", " << result[3] << "}\n";

のように書ける。ここで_mm256_add_pd(v1, v2)が、4つの浮動小数点数の足し算を1命令(vaddpd)で行う命令に変換される。

ただし、SIMDレジスタへのロード・ストアは、メモリ側のアドレスに制約を課す。ロード・ストアの対象は256bit (32byte)幅にアラインしている必要がある(アラインされていない場合は注意してそれなりの扱いをしなければならない)。多くのアーキテクチャは普通そんな幅のアライメントをデフォルトにしない。これはユーザーが保証しなければならないのだ。

実を言うと上の例で使用したstoreu_pdはストアする先が32byteにアラインされていない(unalignedな)場合にもデータを問題なく書き込んでくれるが、オーバーヘッドがかかる。アライメントされている前提で動くstore_pdが存在し、こちらの方が高速に動作する。速度が必要だからSIMDを書いているのであって、オーバーヘッドがかかっては本末転倒である。なのでstore_pdを使える方が望ましい。

SIMD以外にも、キャッシュラインに対応したアライメントに調整することでキャッシュヒット率を最適化するなどの用途があるようだ。

C++11 alignasの問題点

C++11にはalignasalignofstd::aligned_storagestd::alignが導入された。alignasはアライメントを(通常より大きな値に)指定できるものである。

struct alignas(32) d4 {
    double v[4];
};

このようにするとこの構造体は32byte境界にアラインされる――ただし、静的確保した場合のみ。 動的確保した領域は指定したとおりにアラインされている保証はないのだ。つまり、new d4std::vector<d4> vec;のそれぞれの要素が正しくアライメントされている保証はない。 newmallocはあらゆる型のデフォルトのアライメント要求を満たすような領域を確保するが、デフォルトより大きいアライメント要求には応える保証はない。

これは困る。これを回避するには、2つ方法があった。 1つめはメモリを必要量より大きめに確保しておいて、アライメントがあう部分を取り出してそこだけを使うという戦略で、std::alignはその用途にもってこいだ。だがこれだとメモリ管理が異様に複雑になってしまう。 2つめは_mm_malloc/_mm_freeposix_memalign_aligned_mallocなどのアライメントを保証する動的確保関数を使用することだ。だが、これらは非標準であるので、切り替えが面倒だ(それを言うなら_mm_add_psなどはアーキテクチャ依存もいいとこだが)。

C11にはstdlib.haligned_allocが入ったが、C++11には入らなかった。よってBoost.alignなどはコンパイラと環境を見て使用可能な関数をマクロで切り替えている。 言語レベルのアライメントサポートとして、これは片手落ちだ。

C++17におけるアライメントサポート

さて、C++17ではめでたく動的確保時にアライメントを保証できる機能が追加されることになった。C11に追従して<cstdlib>aligned_allocが追加され、またnewについてalign_val_tを取るオーバーロードが追加された(P0035, N4659 § 21.6)。

そのために、align_val_tがまず定義された。意図しない型変換を防ぐため、enum classになっている。ただ、これはopaque typedef相当の利用方法なので特に値が定義されているわけではない。

namespace std {
  enum class align_val_t : size_t {};
}

そして、以下のようなオーバーロードが追加された。

void* operator new(std::size_t, std::align_val_t);
void* operator new[](std::size_t, std::align_val_t);
void operator delete(void*, std::align_val_t);
void operator delete[](void*, std::align_val_t);
void operator delete(void*, std::size_t, std::align_val_t);
void operator delete[](void*, std::size_t, std::align_val_t);

align_val_tとして不適当な値を渡した場合、その動作は未定義である(§ 21.6.2)。

これらの呼ばれ方は少し変わっている。通常のnewの結果よりも大きなアライメントを要求する型についてnew Tを行った時、自動的にalign_val_t版が呼ばれ、そうでない場合は普通のバージョンが呼ばれるのである(§ 21.6.2.1 Effects、§ 21.6.2.2 Effects)。

「通常のnewの結果よりも大きなアライメントを要求する型(new-extended alignment)」は、__STDCPP_DEFAULT_NEW_ALIGNMENT__を超えるアライメント要求を持っているかどうかで判定される(§ 6.6.5)。このマクロはstd::size_t型の整数リテラルに展開され(§ 19.8)、それより大きいアライメント指定を持つ型にはnew(align_val_t(alignof(T)))が呼ばれる。定義済みマクロとして提供される理由は、標準ライブラリヘッダを何一つincludeしていなくてもこの値を知りたい可能性があるからだ。その値を知るためだけに<new>などをincludeする必要はないだろう。

要は、ユーザーはnewに関してあまり頭を悩ませる必要はなく、大きなアライメントを好きに選択すれば、あとは必要に応じて勝手に必要な方のnewが選択して解決されるのだ。素晴らしい。

pitfall

ところで、new/deleteはクラスでオーバーロードすることができる。もし再利用しようとしているC++17以前のコードで、クラスが専用newを持っていたとしたら、そしてそのクラスが通常のnewよりも大きなアライメントを要求するなら、どうなるべきだろうか。

ユーザー定義newは中でメモリ確保以外の副作用を持てるので、もしそれが呼ばれず標準定義align_val_t版が呼ばれた場合、知らぬ間にプログラムが壊れてしまう。これは恐怖である。このことを知らないプログラマは、自分のプログラムを完全に制御できていると思いながら、実際には呼ばれることのないnew演算子を定義しているかもしれないのだ。このような状況はできる限り避けねばならない。今回の場合、クラス特異的なユーザー定義newが提供されている場合はそれが呼ばれる必要がある。

呼び出し解決の順序は以下のようになる。

  1. クラス特異的でアライメント要求ありnew
  2. クラス特異的でアライメント要求無しnew
  3. グローバルでアライメント要求ありnew
  4. グローバルでアライメント要求なしnew

クラス特異的なnewは、アライメント要求に関わらずグローバルなnewより優先される。アライメント要求の有無に関しては、ある方がない方より優先される。