constexpr関数の評価速度

最近のC++では、constexprを使って手軽にコンパイル時計算ができます。 C++20ではstd::vectorstd::stringなどの動的なデータ構造もconstexpr指定されたため、さらに利便性が高まりました。 コンパイル時に多少の探索を行って最適な値を発見して埋め込む、といった利用方法も現実的となりました。

一方で、constexpr関数の評価は実質的にインタプリタ実行であり、速度が気になります。 今回はそれを調査してみました。

測定環境

  • Intel Core i9 12900K (Alder Lake-S)
    • 5.1 GHzくらいで動いている
  • Ubuntu 20.04.3 LTS on WSL2 (Windows 11)
  • g++ (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0(aptでインストールしたもの)
  • clang++ version 12.0.1(公式によるx86_64-unknown-linux-gnu向けプレビルドバイナリ)
  • 時間の測定は何回か実行した平均

実験に使ったソースコード

#include <iostream>

X unsigned long long f( unsigned long long n ) {
        unsigned long long x = 1234567890987654321;
        for( ; n--; ) {
                x ^= x << 7;
                x ^= x >> 9;
        }
        return x;
}

int main() {
        Y unsigned long long x = f( N );
        std::cout << x << std::endl;
}

X, Y, Nはコンパイルオプションで指定します。

g++

g++は、以下の戦略でコンパイル時計算を行うようです。

  • 関数がconstexpr指定されていなくて、変数がconstexpr指定されていなければ、コンパイル時計算を行わない。
  • 関数がconstexpr指定されていなくて、変数がconstexpr指定されていれば、コンパイルエラー(C++の仕様)。
  • 関数がconstexpr指定されていて、変数がconstexpr指定されていなければ、
    • とりあえずコンパイル時計算してみる。
    • 評価回数上限(-fconstexpr-loop-limit-fconstexpr-ops-limitで指定できる)に達したら、コンパイル時計算を打ち切る。
  • 関数がconstexpr指定されていて、変数がconstexpr指定されていなければ、
    • とりあえずコンパイル時計算してみる。
    • 評価回数上限(-fconstexpr-loop-limit-fconstexpr-ops-limitで指定できる)に達したら、コンパイルエラーにする。

実行時間は以下のようになりました。

X Y Opt limit N=1000000 N=10000000
なし なし -O0 default 0.16 s 0.15 s
なし なし -O2 default 0.17 s 0.16 s
constexpr なし -O0 default 0.49 s 0.48 s
constexpr なし -O2 default 1.08 s 1.15 s
constexpr なし -O0 999999999 1.89 s 23.3 s
constexpr なし -O2 999999999 1.97 s 23.6 s
constexpr constexpr -O0 999999999 1.94 s 22.5 s
constexpr constexpr -O2 999999999 1.92 s 23.3 s

普通の命令セットだと一周当たり4cycleくらいで実行可能な命令列にコンパイルされるはずなので、5GHzのCPUで動かすと1秒間に109周以上できるはずです。 107周に23秒かかっているので、コンパイルされたコードの2500倍程度遅いということがわかりました。

clang++

clang++は、変数がconstexpr指定されている時にのみコンパイル時計算を行うようです。

実行時間は以下のようになりました。

X Y Opt limit N=1000000 N=10000000 N=30000000 N=100000000
なし なし -O0 999999999 0.16 s 0.17 s
なし なし -O2 999999999 0.18 s 0.20 s
constexpr なし -O0 999999999 0.20 s 0.18 s
constexpr なし -O2 999999999 0.17 s 0.18 s
constexpr constexpr -O0 999999999 0.83 s 6.8 s
constexpr constexpr -O2 999999999 0.85 s 6.8 s 20 s 67 s

108周に67秒かかっているので、コンパイルされたコードの700倍程度遅いということがわかりました。

まとめ

コンパイル時計算(constexpr関数の評価)は機械語コードの実行と比べて三桁(オーダーで1000倍)程度遅いことがわかりました。