日頃より、弊社をご愛顧いただきまして誠にありがとうございます。この度、ゴーツーラボ 株式会社が営むアトラシアン製品ライセンスの販売および SI サービスの提供事業をリックソフト 株式会社に譲渡することで合意し、事業譲渡契約を締結する運びとなりました。詳細については、「事業譲渡に関するお知らせ」をご参照ください。

パス制御に例外を使用する場合はスタック トレースに書き込まないこと

2011-05-26 (Thu)  •  By 伊藤  •  活用のヒント  •  Java 翻訳

今回の記事は、アトラシアン ブログ「If you use Exceptions for path control, dont fill in the stack trace 」の弊社翻訳版です。原文と差異がある場合は、原文の内容が優先されます。

この投稿にはコードのパフォーマンスや読みやすさに関する一般的な主張が含まれています。これらの分野でのそうした主張はすべて、思考実験や不自然なテス ト ケース、それから大げさな議論によって簡単に反証されることがあります。とは言え、個人的には自分の主張はほぼ正しいと信じています。

例外は遅い

例外のスローはイケてないですよね。遅いしコードが読みにくくなるし。まあ、そんな感じで。

Webwork 1 には名前を値に対して参照するフラットな構成メカニズムがあります。

これは “このキーについて知っているか” と代わる代わる尋ねられ、知っている場合はその値を提示する構成プロバイダーの鎖状のハッシュ マップのようなものです。知らない場合はそのように知らせてもらい、次のものに尋ねます。古典的なトライステート リターン論理です。

さらに Webwork 1 は “そのキーのことは知らない” と言うためのメカニズムとして例外を使用します。これを笑う前に java.util.ResourceBundle がこれとまったく同じことをするのを思い出してください。少なくとも 1.6 まではこのようにされていましたが、それはまた別の問題です。

サンプル コードを以下に示します。

public Object getImpl(final String aName) throws IllegalArgumentException
    {
        // Delegate to the other configurations
        IllegalArgumentException e = null;
        for (final ConfigurationInterface config : configList)
        {
            try
            {
                return config.getImpl(aName);
            }
            catch (final IllegalArgumentException ex)
            {
                e = ex;
                // Try next config
            }
        }
        throw e;
    }

この例では例外のスタック トレースは重要ではありません。また、例外の処理という面から言うとこれが非常に読みやすいコードであると力説したいと思います。IllegalArgumentException を使用するのではなく独自の実行時例外タイプを宣言したかもしれませんが、非常に読みやすいです。

さらに、最後の例外が再スローされたという事実は重要ではありません。周りのコードのほとんどがこれを行うためです。

try
        {
            String classname = (String) Configuration.get(CLASS_NAME);
            if (classname != null && classname.length() > 0)
            {
                try
                {
                    impl = (InjectionImpl) ClassLoaderUtils.loadClass(classname, InjectionUtils.class).newInstance();
                }
                catch (Exception e)
                {
                    LogFactory.getLog(InjectionUtils.class).error("Could not load class " + classname + " or could not cast it to InjectionUtils.  Using default", e);
                }
            }
        }
        catch (IllegalArgumentException e)
        {
            //do nothing - this is just because the property couldn't be found.
        }

そういうわけでスタック トレースは必要ありません。

しかし、すべての例外のルートとして Throwable がすべてのコンストラクターでこれを行うため、いつも発生してしまうのです。

public Throwable() {
        fillInStackTrace();
    }

    public Throwable(String message) {
        fillInStackTrace();
        detailMessage = message;
    }

    public Throwable(String message, Throwable cause) {
        fillInStackTrace();
        detailMessage = message;
        this.cause = cause;
    }

ほら、スタック トレースに繰り返し表れていますよね?

しかし、Jed Wesley Smith が素晴らしい Java のトリックを思い付きました。タック トレースに一切書き込まないことです。

実行速度

例外スタック トレースの書き込みは例外処理で多くの時間を占めます。

これがない場合、

>try {
    throw e;
    ...
} catch (e) {
}

はオブジェクト割り当てと goto ステートメントとなります。

また、スタック トレースを書き込まないことは簡単です。

実際、Jed は既に JIRA ソース コードにサンプルを残しています。

    /**
     * efficient exception that has no stacktrace; we use this for flow-control.
     */
    @Immutable
    static final class ResourceNotFound extends Exception
    {
        ResourceNotFound()
        {}

        @Override
        public Throwable fillInStackTrace()
        {
            return this;
        }
    }

そう、これです。この例外ではスタック トレースは使用しません。e.printStackTrace() を使用しても、単に空になるだけです。私はこれに関するマクロ テストを作成し、スローされたスタック トレースいっぱいの例外とスタック トレースがない例外を異なるレベルのスタック深度で比較しました。

結果、25% 高速であることが判明しました。いや、私は数学が苦手なので実際は 400% アップであると修正されました。

スタック トレースがないと高速です。

初めは Rupert Shuttleworth が機能テスト実行時に JIRA を分析しているときにこれを発見しました。彼はサーバー時間の 5% がこの例外処理に費やされることに気付きました。

リクエストごとに config を検索するための呼び出しは数多くあり、それぞれが約 10 件の config プロバイダーのスタックを委任します。

スタック トレースに書き込みしない例外クラスを使用すると、コードがプロファイル リストから減っていきました。やった!

コードの読みやすさ

トライステート リターン論理を示す Java のタプル オブジェクトを返すことが想像できると思います。以下のようなものもあります。

public static class CompundResult {
        private final Object value;
	private final boolean found;

    	public CompoundResult(final Object value, final boolen found)
	{
		this.value = value;
		this.found = found;
	}

	public boolean found()
	{
		return found;
	}

	public boolean value()
	{
		return value;
	}
    }

    public CompundResult getImpl(final String aName)
    {
        // Delegate to the other configurations
        for (final ConfigurationInterface config : configList)
        {
            CompundResult result =  config.getImpl(aName);
            if (result.wasFound())
	    {
		return result;
	    }
            // Try next config
        }
       return new CompoundResult(null,fase);
    }

しかし、これは少し読みにくく、複合リターン オブジェクトに中間クラスが必要だと思います。例外はもっと読みやすいです。最初はそう思わなかったのですが、例外は遅くて良くないものだという偏見があったようです。

でも、例外がほとんど負担にならないのであれば、もっと優れたコードになりえます。また、このケースでは既存のデザインに縛られていたため、呼び出し元をすべて変更するのは困難でした。

以来、Jed は上記の複合オブジェクトをより良くエンコードするためのさまざまな機能スタイル オプションの使用方法を教えてくれます。ですが、これは別のブログ投稿にとっておきましょう。

要するに遅すぎるということ

教訓として、パス制御に例外を使用する場合はスタック トレースに書き込みをしない例外クラスを使うようにしましょう。

より高速でより読みやすくなります。

ヒントをくれた Jed と、最初にこれを発見した Rupert に感謝します。

  前の記事 次の記事  

関連記事