Help us understand the problem. What is going on with this article?

Clangでコンパイル時に評価されるコード量を増やす

More than 1 year has passed since last update.

TL;DR

clang++ を使用して constexpr を多用するコードをコンパイルするときに、式評価回数の限界に引っかかった場合は、clang++ オプションに-fconstexpr-depth=-1 -fconstexpr-steps=-1 を追加すれば良い。それでもエラーが出る場合はClangにパッチを当てれば良い。

発端

C++17の constexpr を使用してニューラルネットワークの誤差逆伝播を行うプログラムを作成した際に、clang++ のデフォルトの設定ではコンパイル時に十分な回数 式評価が行われず、以下のようなエラーが出た。

  • constexpr evaluation exceeded maximum depth
  • constexpr evaluation hit maximum step limit
  • constexpr evaluation hit maximum call limit

これらのような制限を取り除くための簡単な調査をした。

過程

まずcpprefjpの constexpr の項によると、コンパイル時の再帰回数は -fconstexpr-depth= オプションによって指定でき、デフォルト値は512回である。これを正の方向に増大させると、その最大値が 2147483647 であることが分かる。これは内部的にこの値が unsigned int で保持されていることによる。 1

また、コンパイル時に評価されるステップ数を -fconstexpr-steps= オプションで指定することができ、これの最大値も同様に 2147483647 である。

ところが、Clangのコマンドラインオプション解析においては、オプションの整数値は (signed) int として評価され、それが整数の型変換によって unsigned int に変換されている。そのため -fconstexpr-depth=-fconstexpr-steps=-1 を指定すると 4294967295 を指定したことになる。デフォルトのClangではこれがおそらく最大値である。

問題

C++14以降の constexpr の制限緩和によって forif をそのまま書くことができるようになった。そのため、コンパイル時の再帰回数については以上の解決方法でおそらく問題ない。

一方で最大ステップ数は比較的容易に上限に達しうる。また、コンパイル時の式評価のためにClang内部で用いられている CallStackFrame のインデックスもまた unsigned int で管理されているため、それ以上の関数呼び出しを一関数内で行うことはできない。2

これらの問題を解決するにはClangのソースコードを改変する他にない。

Clangにパッチを当てる

以上の問題を解決するために、2種類のコード改変を行う。

  • ステップ数の管理をしている変数の型を unsigned int から int に変更し、これが負の場合は、処理のステップ数のカウントをやめる。これによって -fconstexpr-steps=-1 と指定すれば、無制限にステップを重ねることができるようになる。
  • CallStackFrame のインデックスの型を unsigned int から size_t に変更する。これによって扱うことができるインデックスの値を $2^{64} - 1$ まで増やすことができる。3

これらの改変を施すパッチがこれである。使用するためには、Clangの公式ドキュメントを参考にしながら、途中で svn patch clang.diff を行えば良い。以下に典型的な手順を載せる。

cd where-you-want-llvm-to-live
svn co http://llvm.org/svn/llvm-project/llvm/trunk llvm
cd llvm/tools
svn co http://llvm.org/svn/llvm-project/cfe/trunk clang
svn patch where-you-cloned-constexpr-nn/clang.diff
cd ../..
mkdir build
cd build
cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Release ../llvm
make -j6

C++の規格

上のパッチに関して、C++の規格上どのような扱いになるのか、気になる向きもあるかもしれない。C++17の規格には4 "Annex B" として "Implementation quantities" という章があり、そこにコンパイル時の再帰回数と評価されるべき式の数の最小値が記載されている。5引用すると

(2.38) Recursive constexpr function invocations [512].
(2.39) Full-expressions evaluated within a core constant expression [1 048 576].

先述のパッチはこれらの制限を満たすため、規格上も問題ないと思われる。

終わりに

C++初心者なので、なにか間違い等あればコメント欄などでマサカリを投げて頂けると幸いです。血を流して喜びます。


  1. unsigned int は厳密に言えば4 bytesとは限らないが、この上限をClang側がどう決めているのかを詳しく調べてはいない。以下同様。 

  2. この表現は適切でないかもしれない。ある関数を呼び出すごとに、呼び出し元の関数の CallStackFrame のインデックスに1足した値を、呼び出し先の CallStackFrame のインデックスとしている。 

  3. もちろん size_t が64 bitsの環境に限る。 uint64_t を使用するほうがより良いかもしれない。 

  4. 正確には、公式のC++規格とほぼ同じとされる、C++17が出る直前のドラフトによる。ここなどから参照できる。 

  5. 正確には推奨される最小値。 

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away