variable tracking size limit exceededと出てコンパイルが遅い場合の対処法

以下の短いコードは、g++ -O2 -g main.cpp -cなどとコンパイルすると長い時間待たされた後、note: variable tracking size limit exceeded with ‘-fvar-tracking-assignments’, retrying withoutと出力され、その直後にコンパイルが終わります。

#include <array>

int f();

struct Elem {
    Elem()  { f(); }
};

struct NTD {
    ~NTD();
};

struct Foo {
    NTD ntd;
    std::array<Elem, 5500> arr;
    Foo();
};

Foo::Foo() : arr{} {}

この現象の発生するコンパイラ

Compiler Explorerで確認したところ、この現象が起こるg++のバージョンは、gcc-4.7.1(-std=c++11が使える最初のg++)からgcc-11.4.0まででした。

この現象の発生条件

以下をすべて満たしたときだと思います。

  • 次をすべて満たすクラス(例ではFoo)がある
    • 次の条件を満たすクラス(例ではElem)が要素のstd::arrayで、それなりの要素数のもの(例ではarr)をメンバ変数として持つ
      • デフォルトコンストラクタ(例ではElem::Elem())の中で例外を送出しうる関数呼び出し(例ではf())を行う
        • 移譲先やメンバ初期化の右辺であっても該当する
        • noexcept指定されていれば該当しない
        • noexcept指定されていなくても、中身を見て例外が出ないと明らかであれば、該当しない
    • そのstd::arrayよりも前に、実質的な中身のあるデストラクタを持つメンバ変数(例ではntd)を持つ
      • 基底クラスも「前」にあるので該当する
      • 非自明だが空のデストラクタ(~NTD() {}とか)は該当しない
      • というか、実質的に何もしないデストラクタは(~NTD() { int i; }とかも含め)全て該当しない
    • コンストラクタ(例ではFoo::Foo())の中でそのstd::arrayを値初期化({}で初期化)する
    • そのコンストラクタのコードが生成される
      • クラス内で定義したけど実際には使わない、とかだとコードが生成されない
  • 最適化オプションをつける
  • -gをつける

出力コードを見たり条件を見たりするとわかりますが、どうやらarrの初期化中に例外が送出されてntdのデストラクタを呼ぶ場合があるかを気にしているようです。

コンパイルにかかる時間

私の環境(Ubuntu 20.04.6 LTS on WSL2, g++-9 (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0, Intel(R) Core(TM) i9-12900K)では、配列サイズを変えたときのコンパイル時間は以下のようになりました。

配列サイズ コンパイル時間
5500 1.4秒
10000 2.5秒
20000 6.2秒
40000 18秒
80000 90秒

となりました。コンパイル時間が超線形に増大していくことがわかります。

note: variable tracking size limit exceeded with ‘-fvar-tracking-assignments’, retrying withoutというエラーメッセージを見ると、コンパイルの途中でなんかの上限に達したからあきらめる、というような動作を想像します。 しかし、このコンパイル時間を見ると、そうではないようです。 どうやら最後まで処理してから、その結果が上限を超えているのでやりなおす、という動作のようです。 なので、上限を変更したりせずにg++をデフォルトの設定で使っているだけなのに、ものすごくコンパイルが遅くなる現象が発生します。

原因

g++でコンパイルが非常に遅くなる短い例 - よーると似ています。

根本的な原因は、std::arrayを値初期化するときに配列要素のコンストラクタ呼び出しループがアンローリングされ、大量のデバッグ情報が生成されることです。

対処法

g++でコンパイルが非常に遅くなる短い例 - よーるとほぼ同じです。 つまり、

  1. 最適化をかけないでコンパイルする
  2. -gをつけないでコンパイルする
  3. g++ではなくclang++を使ってコンパイルする
  4. コンストラクタでの初期化を値初期化ではなくデフォルト初期化(何も書かない)にする(std::arrayはそうなっている。ただし、intのような非クラス型のデフォルト初期化は「未初期化」なので注意しなければいけない)

しかし、4年前と違い、今であれば根本的な解決策があります。

それは、g++-12以降を使うことです。 g++-12以降ではアンローリングされずにちゃんとループになるので、このような問題は発生しません。

なお、variable tracking size limit exceededが出ないようにしたいだけであれば、noexceptをちゃんとつける、といった方法があります。 ただし、結局デバッグ情報が大量生成されることには変わりがない(g++でコンパイルが非常に遅くなる短い例 - よーるの問題が残る)ため、コンパイルは遅いままです。

まとめ

  • 素数が大きいstd::arrayを値初期化すると、コンパイルが遅いことがある
    • 今回紹介したのはnote: variable tracking size limit exceeded with ‘-fvar-tracking-assignments’, retrying withoutと出るパターン
    • 配列要素の初期化中に例外が送出されて配列よりも前に初期化した変数のデストラクタを呼ばれる可能性を追跡しているっぽい
    • 配列要素のコンストラクタ呼び出しループがアンローリングされ、それに伴ってデバッグ情報が大量生成されることが遅くなる原因
  • g++-12以降を使えばアンローリングされないで済むので、このような遅くなる現象は発生しない