ちょっとしたきっかけがあって、C# 7のタプル、何要素まで書けるのかというのにチャレンジすることになりまして。 結果だけ書くと、数万要素のタプルを書くとVisual Studioがクラッシュしました。

これは、別にタプルに限った話ではなくて、巨大なソースコードを食わせてコンパイラーを限界まで酷使したら落ちるのは当然なことでして。 起こしているエラーはスタック枯渇に類するもの(Insufficient Stack)のようです。 コンピューターの資源は有限なので、OutOfMemoryやStaskOverflowなどの資源枯渇系のエラーはどうやっても起こりえます。 数万引数のメソッドを書いたり、数万項の足し算をしたり、数万caseswitchを書いたりしても、同様に落とせると思います。

なので、本当に気にするべきなのは以下のような点かなぁと思われます。

  • 仕様上の上限: 要素数などに仕様上の上限を設けるべきかどうか
  • 資源枯渇への対処: 資源枯渇を事前に察して適切にエラー メッセージを出すすべきか、コンパイラー自体をクラッシュさせるべきか
  • クラッシュ時の対処: コンパイラー自体がクラッシュした際に、IDE側は同エラー処理すべきか

まあ、割かし一般的な「異常系に対する対処」に関する話です。

仕様上の上限

仕様で決まっている例

この手の数の上限、仕様として定めているものもあります。例を挙げると以下のようなものがあります。

  • C++ では、constexprという機能で、再帰呼び出しに上限があります
    • 標準仕様上は「512回以上であることを推奨」という感じで推奨設定が決まっています
    • 実際の上限はコンパイラー依存、かつ、コンパイル オプションで変更できます
  • Java では、メソッドの引数の上限が256個までと決まっています
    • バイトコード上の制約なので、どうやってもこの数を超えることはできません

仕様を決めることの善し悪し

仕様的に上限を決めることには善し悪しあります。

仕様化するためには、結構余裕をもって上限を設定せざるを得ません。コンピューターの性能が上がって、ちょっと頑張れるようになったとしても、仕様で決まっている数を超えれなくて不便だったりします。

その一方で、コンパイラーの限界ぎりぎりまで頑張る場合、あるマシンではコンパイルできたのに、他のマシンではできないということがあり得ます。 今回のタプルに関しても、実際、手元のマシンだと1万要素くらいはコンパイルできましたが、TryRoslynなどのオンライン サービスを使うと1000ちょっとでもうコンパイル エラーを起こす用です(要するに、使っているVMインスタンスがしょぼい)。

個人的に、1000っていうラインは結構微妙なところです。 手書きで1000個のタプル要素や引数を書くことはまずないでしょう。 でも、「コード生成とかしててうっかり」だと、数百~1000は「ありえる」範疇。

で、開発機ではコンパイルできる。なのに、CIとかを掛けていて、毎朝ナイトリー ビルドには失敗している、みたいなことがあり得るわけです。

マシン環境に左右されない意味では、明確に上限が決まっている(少なくともC++のconstexprみたいに「推奨」が決まっている)方がありがたいのはありがたいです。

仕様を決められるか

とはいえ、引数の数や、再帰の段数みたいに具体的な数値を指定しやすいものばかりではありません。

タプルの場合、「要素数」で区切ればいいじゃないかと思うかもしれませんが、実際にエラーの原因になっているのはスタックの深さであって、要素数は直接の原因ではありません。 例えばの話、数千項の足し算の中に、数千要素のタプルと、数千引数のメソッド呼び出しが混在していたりすると、項数/要素数/引数の数の合計によって限界が決まるだろうことは大まかに想像がつくかと思います。 なので、単純にタプル要素数だけに制限を書けるのも変な話でしょう。

もっと難しい例でいうと、型推論をかなり頑張ってる言語では、「型推論に時間が掛かりすぎているのであきらめてエラーにします」みたいなエラーを出すものもあったりします。 時間で言われても、どういう場合なら推論できて、どういう場合ならできないのかがまるで分らず…

資源枯渇時の対処

まあ、割かし仕様としては決めかねるものなので、コンパイラーの限界まで頑張ることにしましょう。 そうなると、資源枯渇した瞬間がコンパイルの限界になります。

ところが、OutOfMemoryにしろStackOverflowにしろ、資源枯渇からの復帰はかなり難しいです。 そりゃまあ、復帰にもメモリを使ったりするわけで、OutOfMemory下でできることとか限られています。

なので、コンパイラーをクラッシュさせないためには、かなり防衛的に、余裕を持った上限設定が必要だったりします。 「再帰呼び出しが1000段を超えたのでエラーにしよう」的な。 結果的に、仕様的に上限を決める場合同様、過剰な制限が掛かって利便性に難ありです。

そこまでしても、どうせ返せるコンパイル エラーは「資源不足なのでこれ以上のコンパイル作業はできません」程度の、何の解決にもならないメッセージのみ。 だったら、OutOfMemoryやStackOverflowをそのまま未処理例外にして、コンパイラー自体がクラッシュした方が費用対効果は高いのではないかということになります。

ただ、その場合、コンパイラーを呼び出している側でクラッシュに対するちゃんとした対処が必要になります。

クラッシュ時の対処

で、まあ、今回Visual Studioがクラッシュしているのがちょうどそんな感じの状況なわけです。 無理なソースコードを食わせたからコンパイラーがクラッシュした。 Visual Studio側がコンパイラーのクラッシュに正しく対応していないみたいで、Visual Studioごとクラッシュする。

これ、Visual Studio Codeだと大丈夫みたいです。 Codeの場合は、急にインテリセンスが働かなくなるだけ。 エディターとしてはフリーズすることなく稼働し続ける。 Codeでは、Language Server Protocolってものを介して、エディターの外のプロセスと通信してインテリセンスを動かしているので、言語サーバー側がクラッシュしても、エディターは無事です。

利用者からすると、エディターごと落ちるとかほんとつらいんで、今のVisual Studioの挙動は結構いやなんですけども。 Visual Studioの、C#コンパイラーの未処理例外に対する耐性の低さは結構きついものがあります。 今でこそ、Visual Studio 2017もRC3 (リリース直前)まで来たのでクラッシュも減っていますけど、 もっと開発初期なら簡単にコンパイラーがクラッシュしますし。 いまだに、例えば「null is」って打つだけでコンパイラーをクラッシュさせれるバグが残っていたりするんですけども、 それでも、Visual Studioごとクラッシュします。

まとめ

数万要素のタプルを書くとVisual Studioが落ちます。

そんなソースコードを食わせたらコンパイラーが落ちるのは割と想定の範囲内なことかと思われます。 この手の問題に対して仕様的に上限を定めるのは、過剰な制限になって利便性を損なったり、 適切な上限の掛け方が見つからなかったり、結構な難しさがあります。

めったに起きない状況に対して過剰なコストをかけてもしょうがないでしょう。 限界に挑戦するようなソースコードを与えられたとき、クラッシュするのが現実解であることは多々あります。

問題は、むしろ、Visual Studio側が、C#コンパイラーのクラッシュに対して脆弱すぎる点かなぁと思います。 同じようなひどいソースコードを与えても、Visual Studio Codeだったらクラッシュしなかったりもします(インテリセンスだけが効かなくなる)。 Visual Studioにも、C#コンパイラーのクラッシュにエディターごと巻き込まれないような対処が必要でしょう。