ククログ

株式会社クリアコード > ククログ > mrubyの例外のバックトレースの実装

mrubyの例外のバックトレースの実装

2015年の12月に改良されるまで、mrubyの例外のバックトレースは壊れていることがありました。どういうときに壊れるかというと、たとえばrescueの中でメソッドを呼ぶと壊れました。

どうして壊れていたかと現在はどうやって壊れないようにしているかを説明します。

壊れていた理由

まず壊れていた理由を説明します。壊れていた理由は、バックトレースを取得するとき(Exception#backtraceを呼び出すとき)に「そのときのスタックからバックトレース情報を構築していた」からです。

この方法は例外発生時のスタックの状態とバックトレース取得時のスタックの状態が同じなら問題はありません。しかし、メソッドを呼び出す、例外を保存しておいて後でバックトレースを取得する、などスタックの状態が異なるときは壊れたバックトレースになります。

なお、このような実装になっていた理由は(おそらく)パフォーマンスです。このように(バックトレース取得リクエストがあるまでバックトレース構築を遅延)せずに、例外発生時にバックトレースを構築する実装にすると、例外をあげるコストが高くなります。例外のバックトレースは必ず使われるものではなく、使われないこともあります。そのため、バックトレースを構築しないで済ませられるならしないようにすることで例外をあげるコストを低くできます。実際、CRubyも2.0からバックトレース構築を遅延させるようにして例外をあげるコストを低くしています。

壊れないようにする方法

それではどのようにして壊れないようにしているかを説明します。

作戦は次の通りです。

  • バックトレース情報の構築を次の2段階に分ける

    • Rubyオブジェクトを生成しない生のバックトレース情報の取得(動的にメモリーを確保しないので軽い。実装でいうとmrb_save_backtrace()。)

    • ↑の生のバックトレース情報からRubyオブジェクトにする(動的にメモリーを確保するので重い。実装でいうとmrb_restore_backtrace()。)

  • 生のバックトレース情報の取得は例外発生時に実行する(実装でいうとmrb_exc_raise()

  • 生のバックトレース情報からRubyオブジェクトにするのは必要になるまで遅延する(実装でいうとmrb_exc_set()

Rubyオブジェクトにするところをいかに遅延させるかがパフォーマンスに影響します。具体的には以下のタイミングまで遅延させます。

  1. Exception#backtraceを呼び出したとき

  2. 次の例外が発生したとき

1つめのタイミングは自明です。必要とされているタイミングだからです。

2つめのタイミングは実装の制限です。動的にメモリーを確保しないで「Rubyオブジェクトを生成しない生のバックトレース情報の取得」を実現するために、あらかじめ用意しておいた領域(mrb_state::backtrace)に保存するようにしています。この領域が1つしかないので複数の生バックトレース情報を保存できないのです。そのため、次の例外が発生したときは前の例外の生バックトレース情報をRubyオブジェクトにして、代わりに次の例外の生バックトレース情報を保存するようにしています。

こうすることによりmrubyでは例外をあげるコストを低くしています。

まとめ

mrubyの例外のバックトレースの構築を遅延させている実装がどうしてこうなっているのか(mrb_exc_set()がどうして必要なのか)わからないという声を聞いたので、どうしてこのような実装になっているかを説明しました。

クリアコードではCRubyだけでなくmrubyに関する開発・開発支援も承っています。mrubyを使ったアプリケーションだけでなく、mruby本体の改良・修正まで対応できますので、興味のある方はお問い合わせください。