オプション沼(14) 最適化で以下省略なコード書いてみました。gcc -O3オプション

Joseph Halfmoon

別シリーズのRust記事にて時間計測しようとしたらばコンパイラの最適化のために計測対象が「消えてた」件これあり、ありがち?今回こちらでは消えて(飛ばされてしまう)c言語コードをあえて作成しgccの最適化オプションの「威力」を痛感してみたいと思います。最適化は奥深いのでまたやるつもりですが。

※『オプション沼』投稿順indexはこちら

※動作確認に Windows11 WSL2上のUbuntu 20.04 LTS を使用しています。gccのバージョンは9.4.0です

最適化でなかったことにされるコード

なにかベンチマーク的なコードを書いたときにやりがちじゃないかと思います。「いろいろ計算しているけど、ほとんど何もしてない」ことをコンパイラ様に見抜かれてしまうコードを書いてしまうこと。現代のコンパイラ様はそんなコードをお目こぼしはしません。バッサリ。

今回はあえて、「多分消えるんじゃないか」というコードを書いて、gccの-O3オプションを味わってみたいと思います。多分、-O3でも-O2でも変わらんと思うが。ターゲットの「意味なし」C言語コードが以下に。別シリーズのRust記事にあわせて雰囲気だけは行列積を求める感じのもの。

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

#define ARRAYSIZE (100)


int main(int argc, char const *argv[])
{
    uint32_t array_a[ARRAYSIZE][ARRAYSIZE];
    uint32_t array_b[ARRAYSIZE][ARRAYSIZE];
    uint32_t array_c[ARRAYSIZE][ARRAYSIZE];

    for (int y=0; y<ARRAYSIZE; y++) {
        for (int x=0; x<ARRAYSIZE; x++) {
            array_a[y][x] = 0;
        }
    }
    for (int y=0; y<ARRAYSIZE; y++) {
        for (int x=0; x<ARRAYSIZE; x++) {
            array_b[y][x] = 0;
        }
    }
    for (int y=0; y<ARRAYSIZE; y++) {
        for (int x=0; x<ARRAYSIZE; x++) {
            array_c[y][x] = 0;
            for (int i=0; i<ARRAYSIZE; i++) {
                array_c[y][x] += array_a[y][i] * array_b[i][x];
            }
        }
    }

    printf("[5][5] = %d\n", array_c[5][5]);
    return 0;   
}

最適化せずにビルドしたときのコマンドラインが以下に。後で gdb で動作の様子を探るので、-g オプション付けとります。

$ gcc -o tstOptZ0 -g -O0 tstOptZ.c

最適最強?の-O3オプションのときのコマンドラインが以下に。後で gdb で動作の様子を探るので、-g オプション付けとります。

$ gcc -o tstOptZ3 -g -O3 tstOptZ.c

ビルドした結果のファイルサイズが以下に。あれれ、最適化した筈の-O3の方がデカいです。最適化の都合ってもんがあるみたいデス。大人の事情ね。buildResults

最適化しない場合の挙動(gdb制御下)

折角 -g オプションつけて gdb 使えるようにしてあるので、オブジェクトへ飛び込んで step実行し、どんな塩梅だか眺めてみます。最適化していないオブジェクトの起動が以下に。O0gdb0

main()関数の頭にブレークポイントを置いて、run。

ブレークしたらば、レジスタイメージなどが生成されるので、display/i $pcで停止したときに毎回プログラムカウンタのアドレスの命令を表示するようにしておきます。念のため。

step実行していくと、ソースそのままに1行づつくるくる回っておることが分かります。O0gdb2

流石にループを回るのは付き合いきれないので continue すると、末尾にいたって要素[5][5]の値は0だと表示して終了します。O0gdb3

当然だけれど、非最適化の場合はソースそのままでありますな。

最適化した場合の挙動(gdb制御下)

つづいて最適化最強?オブジェクトの起動が以下に。O3gdb0

main()関数の頭にブレークポイントを置いて、run。O3gdb1

ここまでは非最適化と同じですな。同様に display/i $pc をしかけた上で、step実行。mainの頭から1 step で最初のループの先頭にたどり着くところまでは非最適化と同じです。しかしそこで step するとほれこのようにO3EC

途中の行に止まることなく、末尾へ飛びました。結果の表示は期待どおりですがこの結果はコンパイラ様はコンパイル時にお見通しっす。「想定される結果を表示して終わる」コードが生成されてるみたいです。

見事にスキップされてしまったみたいで、予定通り。よかったというべきか今回は。

オプション沼(13) コードカバレッジを計測する準備、gcc –coverageオプション へ戻る

オプション沼(15) インクルードガードの闇?-Wno-import、#importは非推奨 へ進む