C 言語における最適化を抑止する

C 言語の最適化抑止について調査した。

ローカル変数のアドレスを関数外部にリークさせた場合、volatile がなくても最適化が抑止されるのではないか、という仮説を証明するためだ。

環境は以下のとおり。


検証するコードは以下のとおり。

#include <stdio.h>

int *valp;

int main(int argc, char *argv[]) {
    int val = 123;
    printf("%d\n", val);
    valp = &val;
    printf("%d\n", val);
    return 0;
}

グローバル変数 valp にローカル変数 val のポインタを保存する。実際には保存後、2 回目の printf までの間、valp を経由して val の値がどこかで(例えば別スレッドから)書き換えられる可能性があるものとする。

このコードを最適化なし(-O0)でコンパイルすると、val の値が変わっても大丈夫なコードが吐かれた。最適化なしなので当然かも知れないが。

    movl    $123, 28(%esp)  ; スタック(28) に 123 を積む(int val = 123 相当)
    movl    28(%esp), %edx  ; スタック(28) から edx に読み込み
    movl    $.LC0, %eax     ; "%d\n" を eax に読み込み
    movl    %edx, 4(%esp)   ; edx をスタック(4) に積む(printf 第二引数)
    movl    %eax, (%esp)    ; eax をスタック(0) に積む(printf 第一引数)
    call    printf          ; printf 実行
    leal    28(%esp), %eax  ; スタック(28) のアドレスを eax に読み込む
    movl    %eax, valp      ; eax をグローバル変数 valp に読み込み
    movl    28(%esp), %edx  ; スタック(28) から edx に読み込み(■ valp 経由で値が変わっても大丈夫)
    movl    $.LC0, %eax     ; "%d\n" を eax に読み込み
    movl    %edx, 4(%esp)   ; edx をスタック(4) に積む(printf 第二引数)
    movl    %eax, (%esp)    ; eax をスタック(0) に積む(printf 第一引数)
    call    printf          ; printf 実行


最適化あり(-O2)でも同様に、val の値が変わっても大丈夫なコードが吐かれた。最適化はちゃんと仕事をしているのか少し心配になってきた。

    movl    $123, 28(%esp)  ; スタック(28) に 123 を積む(int val = 123 相当)
    movl    $123, 8(%esp)   ; スタック(8) に 123 を積む
    movl    $.LC0, 4(%esp)  ; スタック(4) に "%d\n" を積む
    movl    $1, (%esp)      ; スタック(0) に 1 を積む
    call    __printf_chk    ; __printf_chk 実行
    leal    28(%esp), %eax  ; スタック(28) のアドレスを eax に読み込む
    movl    %eax, valp      ; eax をグローバル変数 valp に読み込み
    movl    28(%esp), %eax  ; スタック(28) から eax に読み込み(■ valp 経由で値が変わっても大丈夫)
    movl    $.LC0, 4(%esp)  ; スタック(4) に "%d\n" を積む
    movl    $1, (%esp)      ; スタック(0) に 1 を積む
    movl    %eax, 8(%esp)   ; スタック(8) に eax を積む
    call    __printf_chk    ; __printf_chk 実行


ここでコードを以下のようにコメントアウトし、ローカル変数のアドレスが関数外部にリークしないようにした。

#include <stdio.h>

int *valp;

int main(int argc, char *argv[]) {
    int val = 123;
    printf("%d\n", val);
//  valp = &val;
    printf("%d\n", val);
    return 0;
}


これを最適化あり(-O2)でコンパイルしたところ、val はスタックに積まれず即値が使われるようになった。目に見えて最適化されていて安心した。

    movl    $123, 8(%esp)   ; スタック(8) に 123 を積む
    movl    $.LC0, 4(%esp)  ; スタック(4) に "%d\n" を積む
    movl    $1, (%esp)      ; スタック(0) に 1 を積む
    call    __printf_chk    ; __printf_chk 実行
    movl    $123, 8(%esp)   ; スタック(8) に 123 を積む(■ 即値が使われる)
    movl    $.LC0, 4(%esp)  ; スタック(4) に "%d\n" を積む
    movl    $1, (%esp)      ; スタック(0) に 1 を積む
    call    __printf_chk    ; __printf_chk 実行


今回の環境では、仮説のとおり volatile がなくても最適化が抑止された。

吐かれたアセンブラコードを見ると、コード内でローカル変数のアドレスが必要ないため、値をスタックに積む必要もなく、したがって即値を使用しているように見えたので、以下のコードで試してみた。

#include <stdio.h>

int main(int argc, char *argv[]) {
    int val = 123;
    int *valp;
    printf("%d\n", val);
    valp = &val;
    printf("%d\n", val);
    printf("%d\n", *valp);
    return 0;
}

最適化(-O2)コンパイルで吐かれたコードはこちら。

    movl    $123, 8(%esp)   ; スタック(8) に 123 を積む
    movl    $.LC0, 4(%esp)  ; スタック(4) に "%d\n" を積む
    movl    $1, (%esp)      ; スタック(0) に 1 を積む
    call    __printf_chk    ; __printf_chk 実行
    movl    $123, 8(%esp)   ; スタック(8) に 123 を積む
    movl    $.LC0, 4(%esp)  ; スタック(4) に "%d\n" を積む
    movl    $1, (%esp)      ; スタック(0) に 1 を積む
    call    __printf_chk    ; __printf_chk 実行
    movl    $123, 8(%esp)   ; スタック(8) に 123 を積む
    movl    $.LC0, 4(%esp)  ; スタック(4) に "%d\n" を積む
    movl    $1, (%esp)      ; スタック(0) に 1 を積む
    call    __printf_chk    ; __printf_chk 実行

なるほど、valp にアドレスは保持させたものの、そのアドレスを使用していないため最適化で即値が使用されてしまっている。


コードを以下のように変えて、アドレスを使用するようにした。

#include <stdio.h>

int main(int argc, char *argv[]) {
    int val = 123;
    int *valp;
    printf("%d\n", val);
    valp = &val;
    printf("%d\n", val);
    printf("%p\n", valp);
    return 0;
}

これの最適化(-O2)コンパイルで吐かれたコードはこちら。

    movl    $123, 28(%esp)  ; スタック(28) に 123 を積む
    movl    $123, 8(%esp)   ; スタック(8) に 123 を積む
    movl    $.LC0, 4(%esp)  ; スタック(4) に "%d\n" を積む
    movl    $1, (%esp)      ; スタック(0) に 1 を積む
    call    __printf_chk    ; __printf_chk 実行
    movl    28(%esp), %eax  ; スタック(28) を eax に読み込む(■ valp 経由で値が変わっても大丈夫)
    movl    $.LC0, 4(%esp)  ; スタック(4) に "%d\n" を積む
    movl    $1, (%esp)      ; スタック(0) に 1 を積む
    movl    %eax, 8(%esp)   ; スタック(8) に eax を積む
    call    __printf_chk    ; __printf_chk 実行
    leal    28(%esp), %eax  ; スタック(28) のアドレスを eax に読み込む
    movl    %eax, 8(%esp)   ; スタック(8) に eax を積む
    movl    $.LC1, 4(%esp)  ; スタック(4) に "%p\n" を積む
    movl    $1, (%esp)      ; スタック(0) に 1 を積む
    call    __printf_chk    ; __printf_chk 実行

ここまで確認してわかったことは、ローカル変数のアドレスが関数外部にリークすると最適化が抑止されるわけではなく、ローカル変数のアドレスが何らかの形で使用されることで最適化が抑止されるようだ。


おまけ。volatile をつけた場合、即値が使われるのかどうかも調べてみた。

#include <stdio.h>

int main(int argc, char *argv[]) {
    volatile int val = 123;
    volatile int *valp;
    printf("%d\n", val);
    valp = &val;
    printf("%d\n", val);
    printf("%d\n", *valp);
    return 0;
}

アセンブラのコメントは省略するが、volatile がきちんと働き、最適化が抑止され、毎回スタックから読み出されるようになった。即値 123 が一回しか使われていないのがポイントで、変数のアドレスを valp に保持してからようやくスタックから読み出すのと異なり、最初からスタックから読み出すようになった。

    movl    $123, 28(%esp)
    movl    28(%esp), %eax
    movl    $.LC0, 4(%esp)
    movl    $1, (%esp)
    movl    %eax, 8(%esp)
    call    __printf_chk
    movl    28(%esp), %eax
    movl    $.LC0, 4(%esp)
    movl    $1, (%esp)
    movl    %eax, 8(%esp)
    call    __printf_chk
    movl    28(%esp), %eax
    movl    $.LC0, 4(%esp)
    movl    $1, (%esp)
    movl    %eax, 8(%esp)
    call    __printf_chk