超例外=メタデータ=コードが速くなるぜ理論

例外原理主義 - TrickDiary

超例外原理主義

このナンセンス感、嫌いじゃないです。

ただ、最近、何もかもがメタデータ(属性・アノテーション)に見える僕にとっては、別のアプローチの方がおもしろいのではないかと思う部分でもあります。
個人的には、assertと例外の関連性あたりから攻めていきたいテーマです。

assert

assertとはいわば、メタデータであるというのが僕の認識です。下手をすると、コメントに成り下がりますが、それでもdoxygenで生成されるドキュメント程度の意味はあるでしょう。

switch(a)
{
  case 0:
    something();
    break;
  case 1:
    theOtherThing();
    break;
  default:
    ASSERT(0); // ここには来ない。はず。
}

といったコードを書く人の気持ちは分かりません。しかしながら、

#define ASSERT(e) (__assume(e))

などと定義されているのであれば話は別です。これは、ASSERTが__assumeというコンパイラの最適化に利用できる可能性のあるメタデータに変身するからです。

一方で、ライブラリ如きがメインプログラムの意志に反して勝手に死ぬの禁止教(別名:abortすんな教)の信者である僕にとっては、

#define ASSERT(cond) throw new std::runtime_error(# cond)

なんてコードには何の抵抗も感じません。こっちだと上位のコードでフックできる可能性が残されるので、安心して寝ることが出来ます。

例外

少しだけ話をすり替えて、Javaの話にすると、Javaでは、メソッドが例外を放つ場合、そのメソッドには必ず発生する可能性のある例外を列記しなければなりません。

void someMethod() throws FooBarException
{
  ...
}

つまり、この時点で、コンパイラは、このメソッドが陥る例外状態に関してのメタデータを得ることが出来ると言えます。現状のコンパイラがこの情報をどの程度有効に使っているのかについては一切知りませんが、起きうる実行時の問題について、事前に情報を得られることは、何らかの実行時最適化に寄与できる可能性があるでしょう。

まぁ、Javaに関して言えば、こんな風に記述しなくても、プログラムを見れば起きうる例外状態は分かるだろうという話はあります。実際、throwsをちゃんと書かないとエラーになりますし。

また、C++でも例外処理に関しては、いわゆる例外フレーム(g++がらみだとこういう悲しい話もある)というものを作成するので、そういう意味では、別にJavaに限らず、バイナリコードの中に例外は深く刻み込まれるのです。

エラーコード

そして、私が目の敵にするのがいわゆる、エラーコードと言われる物です。理由は簡単で、コードがバケツリレーになってしまうからです。

ErrorCode someFunc()
{
  ErrorCode err = anotherFunc();
  if(err != Success)
    return err; // バケツリレー

  ...
  return Success;
}

ErrorCode anotherFunc()
{
  ErrorCode err = yetAnotherFunc();
  if(err != Success)
    return err; // バケツリレー

  ...
  return Success;
}

ErrorCode yetAnotherFunc()
{
  AbsolutelyDifferentFunc(); // あぁ、エラー処理が面倒になってきた・・・
  return Success;
}

この様な雑多なif文はプログラマを憂鬱にするだけでなく、プログラマがエラー処理を省略するという最悪の結末を招くきっかけとなります。

ただし、エラーコードのスキームのまずさは、もっと別の部分にあります。それは、エラー処理がifによって行われる場合、if文のthenとelse、どちらが主処理で、どちらが例外的な処理なのかが、簡単には分からなくなると言うことです。
これは例によって、UNIX系では、-1がエラーで、0が正常終了などの関数が多く、Windowsでは、戻り値がBOOLの関数が多いなど、文化的な背景を理解すればなんとなく分かるという部分はありますが、それでも、

if(!someFunction())
{
  // ケースA
}
else
{
  // ケースB
}

といったコードで、どちらが主処理といったことを知るには、コードの詳細をチェックする以外にないでしょう。

そして、さらに悪いことに、このようなif文は、最近の高度な分岐予測、投機的実行の機能を持つCPUを容易に攪乱することになります。プログラム言語としては、何らかの形で、どっちが主処理なのかをコンパイラ、CPUに明示できると良い部分でしょう。

ところが、このコードを例外をもって書き換えれば、

if(!someFunction())
{
  // ケースA
}
else
{
  // ケースB
  throw std::exception("不味いことが起きたよー!");
}

これは、コンパイラにとっても、ケースBが例外的であり、ケースAが主処理であることはある程度簡単に類推することが可能になります。

例外でも、内部的にはifによって処理されているじゃないかという議論はあるでしょうが、例外処理によって生成される条件分岐のインストラクションに関しては、コンパイラがバイナリコードに「これは例外処理である(分岐予測では分岐予想率を主処理に比べて著しく下げる)」というメタデータを埋め込むことによって、CPUを無駄に混乱させることを防ぐことは十分に可能でしょう。

結論

結局、(少なくとも私には、)assert・例外はメタデータであるというのが結論であるように見えます。雑多なエラー処理コードのifからは、それが例外的な処理であると行った情報を自動的に得ることは難しいでしょう。しかしながら、例外処理を使うことによって、コンパイラがバイナリコードにCPUの分岐処理に有効かもしれないメタデータを追加できる可能性を持たせることが出来るわけです。

そして、この帰結からは、逆説的に、例外を乱用すべきでない理由も見えてきます。それは、「例外処理」はCPUに対して、例外的な処理であると通知されるわけですから、本質的に例外ではない処理に例外を使うことは避けるべきでしょう。

例外が静的なメタデータに過ぎないと考えると、C++ templateのような静的な解析によるコード処理にブレイクスルーのようなものが来ないかなぁ・・・・まぁ、来ないだろうなぁなどと妄想も出来るんじゃないかと常々考えているわけです。