アライメントを知る

2020年1月19日

世の中のプログラマーの大半は、アライメントというものを知らないし、知る必要もありません。

ですが、組み込みプログラマーは別です。もしこの用語が初耳なら、概要だけでも理解しておきましょう。最悪の場合、プログラムが停止する問題に発展することもあるからです。

とはいえ、難しい話ではありません。ここで要点を整理しますので、ぜひ覚えて帰ってください。

結論

とにかく頭に入れておくべき結論は、これです。

N バイトの変数のアドレスは N の倍数でなければならない

アライメントとは、このルールを意識した行為全般を指す言葉です。

問題点

通常の動作

C であれ C++ であれ、本来はコンパイラが常にこのルールを満たすように取り計らってくれますから、ほとんどの場合、プログラマーは何も意識する必要はありません。たとえば――

int16_t a;

と書けば、a のアドレスは必ず2の倍数になりますし――

int32_t b;

と書けば、b のアドレスは必ず4の倍数になってくれます。char を除くプリミティブ型(int、short、long、float、double)の場合、そのサイズは処理系に依存しますが、考え方は同じです。

パディング

次の場合はどうでしょう。

struct {
  int16_t a;
  int32_t b;
} tuple;

ここで a と b を隙間なく配置すると、a のアドレスが2の倍数でも b のアドレスが4の倍数にならないケースが生じてしまいます。大半のコンパイラは、a のアドレスも4の倍数にしてしまい、2バイトの隙間を空けて b を配置することで、両方の要求を満たそうとします。この隙間をパディングといいます。

ただし、パディングの法則はあくまで処理系依存である、つまり言語仕様ではないという点に注意してください。たとえば、内部的に a と b の前後関係をひっくり返して解決を図るコンパイラがあるかもしれません(私は見たことありませんが)。

いずれにせよ、コンパイラはここまでやってくれますから、一見プログラマーの出る余地はないように思えます。

失敗例

組み込みプログラマーは、コンパイラがせっかく設けてくれた安全柵を乗り越えなければならないときがあります。すなわち、ポインタを使うときです。

アライメントの地雷を踏むための最も簡単なコードがこれです。

int32_t getValue(const char *p)
{
  return *reinterpret_cast<const int32_t *>(p);
}

この関数を実行すると、75%の確率でプログラムが吹っ飛びます。p が運よく4の倍数なら助かりますが、char のポインタである p は、4の倍数どころか偶数である保証すらありません。reinterpret_cast が忌み嫌われている理由もここにあります。

ところが、reinterpret_cast を使わなければ安全とも言い切れません。次のコードは、本質的に上のコードと同じ危険性をはらんでいます。

int32_t getValue(const void *p)
{
  return *static_cast<const int32_t *>(p);
}

つまりポインタのキャストを行うときはいつでも、それがアライメントのルールに抵触するおそれがないかどうか、プログラマーは注意を払う義務があるということです。

何が起こるのか

先ほどはプログラムが吹っ飛ぶと表現しましたが、アライメントのルールに反したときの挙動は、実は CPU によります。大別すると、CPU 例外が発生してプログラムが吹っ飛ぶか、速度を落としながらも成功するかのどちらかです。

ただ私は個人的に、たとえアライメントの問題がない CPU を使い、その挙動を理解していたとしても、それに依存したプログラムは書くべきでないと考えます。それは、吹っ飛ぶ CPU のほうが多数派だからです。移植性の話ではありません。この問題を無視する癖をつけてしまうと、プログラマーとして結局困ることになると思うのです。

解決策

素性の知れないポインタを経由して値を抜き取るとき、あるいは値を与えるときは、memcpy を使いましょう。memcpy なら、アライメントを意識した実装が必ずなされています。私はテンプレートと組み合わせて次のような関数を作り、利用しています。

template <class T>
T extract(const void *p)
{
  T val;
  memcpy(&val, p, sizeof(T));
  return val;
}

こうしておけば、次のようなシンプルなコードで安全に抜き取りができるようになります。

int val = extract<int>(p);