x64 CPUのアライメントフォールトのペナルティを調べた

# この記事はなんぞや

別の記事を書くにあたって「そういえば、ずっと昔に x86 でのアライメントフォールト(alignment fault)のペナルティの話を聞いたけど、今でも影響あるんかなー」と思って調べてみた。

結論: **ある**

# 前提

- アライメントとは？
  - こちらを参照: [https://ja.wikipedia.org/wiki/データ構造アライメント](
https://ja.wikipedia.org/wiki/%E3%83%87%E3%83%BC%E3%82%BF%E6%A7%8B%E9%80%A0%E3%82%A2%E3%83%A9%E3%82%A4%E3%83%A1%E3%83%B3%E3%83%88)
- これが起きたら困ることはあるの？
  - **ないです** <br/>普通にプログラムを書く限り問題になることはないです。ただ、CPUの挙動について調べただけです

**(追記 2020/04/24)** すごい、このページ分かりやすいです！
http://www5d.biglobe.ne.jp/~noocyte/Programming/Alignment.html

**(追記 2020/07/10)** 再現できない環境もあったので、[いじりました](#%E8%BF%BD%E8%A8%9820200710)。もはや、どうやればアライメントフォールトを発生させられるか、といったゲームになっているので、普通はまったく意識しなくていいですね……。

# テストコード

`ALIGN` 構造体の `padding` が `unsigned long long` (8バイト)なので、その次の `char[9]` の先頭も8の倍数バイトでアラインされていることが期待できます。

ここで `char[9]` の `align.base` を、先頭から8バイト分 `unsigned long long` として扱うポインタ `*p` を用意します。ここからポインタをぐりっといじって `*p` の先頭を1バイトずらします。
これでアライメントフォールトが発生するはずです。

```c:align.c
#include <stdio.h>

struct ALIGN {
    unsigned long long  padding;
    char                base[9];
};

int main(int argc, char **argv)
{
    unsigned long long  i = 0L;
    unsigned long long  loop_count = 1000000000L;
    unsigned long long   *p;

    struct ALIGN        align;

    if ( argc == 1 ) {
        p = (unsigned long long *) (((char *) &align.base));
        printf( "aligned: %p\n", p );
    }
    else {
        p = (unsigned long long *) (((char *) &align.base + 1));
        printf( "not aligned: %p\n", p );
    }

    *p = 0;
    for ( i = 0; i < loop_count; i++ )
        *p = *p + 1;

    return 0;
}
```

# 実行結果

引数を付けずに実行するとアラインされたまま、なにか引数を付けるとアライメントをずらした `unsigned long long` に代入するのを10億回繰り返します。

とりあえず実行してポインタの値を確認すると、アドレスが奇数になっているので、アライメントが合っていないのが分かります。

```console
$ gcc alignment_fault.c
$
$ ./a.out
aligned: 0x7ffee1cf0a48
$ ./a.out 1
not aligned: 0x7ffee4c09a39
```

実行時間を計ります。

```
$ time ./a.out
aligned: 0x7ffeead84a48
./a.out  1.14s user 0.00s system 99% cpu 1.141 total
$ time ./a.out
aligned: 0x7ffee36f6a48
./a.out  1.14s user 0.00s system 99% cpu 1.145 total
$ time ./a.out
aligned: 0x7ffee8004a48
./a.out  1.15s user 0.00s system 99% cpu 1.152 total

$ time ./a.out 1
not aligned: 0x7ffeeba19a39
./a.out 1  2.18s user 0.00s system 99% cpu 2.187 total
$ time ./a.out 1
not aligned: 0x7ffee1d28a39
./a.out 1  2.20s user 0.00s system 99% cpu 2.201 total
$ time ./a.out 1
not aligned: 0x7ffee0aefa39
./a.out 1  2.21s user 0.00s system 99% cpu 2.215 total
```

このように、2倍弱の遅れが生じました。ほほー。
古いマシンで試したことはないから、昔に比べてどうだとかは分からないですが。

# 追記(2020/07/10)

環境によって差が出たり出なかったりすると思って gcc のマニュアルを眺めていたんですが、こんなことが書いてありました。

https://gcc.gnu.org/onlinedocs/gcc/x86-Options.html
> **-malign-data=type**
> Control how GCC aligns variables. Supported values for type are ‘compat’ uses increased alignment value compatible uses GCC 4.8 and earlier, ‘abi’ uses alignment value as specified by the psABI, and ‘cacheline’ uses increased alignment value to match the cache line size. ‘compat’ is the default.

ひい。gcc は何もしなくてもアライメントを合うように設定するようです。アドレスを表示させたときに奇数になったのはなんでだ……。

あとスタックに乗るときにもデフォルトでは調整されるらしい[^stackrealign]ので、ヒープに乗せるように変更しました。

[^stackrealign]: `-mstackrealign` Realign the stack at entry. On the x86, the -mstackrealign option generates an alternate prologue and epilogue that realigns the run-time stack if necessary. (https://gcc.gnu.org/onlinedocs/gcc/x86-Options.html)

```c:align2.c
#include <stdio.h>

struct ALIGN {
    unsigned long long  padding;
    char                base[9];
};

struct ALIGN align;

int main(int argc, char **argv)
{
    unsigned long long  i = 0L;
    unsigned long long  loop_count = 1000000000L;
    unsigned long long   *p;

    if ( argc == 1 ) {
        p = (unsigned long long *) (((char *) &align.base));
        printf( "aligned: %p\n", p );
    }
    else {
        p = (unsigned long long *) (((char *) &align.base + 1));
        printf( "not aligned: %p\n", p );
    }

    *p = 0;
    for ( i = 0; i < loop_count; i++ )
        *p = *p + 1;

    return 0;
}
```

gcc のオプションに `-malign-data=abi` を付けます。
あと、gcc は最適化オプションを指定しないときは `-O0` とみなすのですが、最適化を無効にするために[^optimize]明示的に付け足しました。 


[^optimize]: コメントで教えていただいたのですが、`-O0` は最適化を完全に無効にするわけではないとのこと。 `gcc -Q -O0 --help=optimizers` で `-O0` のときの[最適化内容が分かる](https://qiita.com/keys/items/d88c069852feff246f6e#comment-94ad3fceedf5d2f9d18a)そうです。ありがとうございます。@fujitanozomu


```
gcc -O0 -malign-data=abi align2.c
```

結果は以下の通り。

```
$ time ./a.out
aligned: 0x601038
./a.out  2.25s user 0.00s system 99% cpu 2.254 total
$ time ./a.out
aligned: 0x601038
./a.out  2.25s user 0.00s system 99% cpu 2.251 total
$ time ./a.out
aligned: 0x601038
./a.out  2.26s user 0.00s system 99% cpu 2.266 total

$ time ./a.out 1
not aligned: 0x601039
./a.out 1  2.72s user 0.00s system 99% cpu 2.720 total
$ time ./a.out 1
not aligned: 0x601039
./a.out 1  2.72s user 0.00s system 99% cpu 2.719 total
$ time ./a.out 1
not aligned: 0x601039
./a.out 1  2.71s user 0.00s system 99% cpu 2.712 total
```

期待する結果出ました。

というかここまで無理くりやらないとダメなくらい、コンパイラは当たり前にアライメントの最適化をするってことですな……。

ちなみに `-O(-O1)` だとどちらも 0.38 秒ぐらいになります。(ちなみにちなみに `-O2` だと 0.001 秒ぐらいになります)