読者です 読者をやめる 読者になる 読者になる

式木の構築が思っていた以上に高コストだった件

.net

式木は評価が高コストで、構築はそれほどでも無いと思っていたら、どうやら勘違いだったみたい。

サンプルを作って実験してみた。

using System;
using System.Diagnostics;
using System.Linq.Expressions;

namespace RequiresSample
{
    public static class ExpressionUtil
    {
        // 式木から変数名を取得する
        public static string GetMemberName<T>(Expression<Func<T>> lambda)
        {
            var memberEx = (MemberExpression)lambda.Body;
            return memberEx.Member.Name;
        }
    }

    public static class Requires
    {
        // 変数名を式木から取得する
        public static void NotNull<T>(T value, Expression<Func<T>> lambda)
        {
            NotNull(value, ExpressionUtil.GetMemberName(lambda));
        }

        // 変数名を文字列で取得する
        public static void NotNull<T>(T value, string name)
        {
            if (value == null)
            {
                throw new ArgumentNullException(name);
            }
        }
    }

    public static class RequiresEx
    {
        // 変数名を式木から取得する
        // 式木の評価は例外を発生させるときまで遅延
        public static void NotNull<T>(T value, Expression<Func<T>> lambda)
        {
            if (value == null)
            {
                throw new ArgumentNullException(ExpressionUtil.GetMemberName(lambda));
            }
        }
    }

    class Program
    {
        private const int COUNT = 100000;

        static void Main(string[] args)
        {
            string key="test";

            // 変数名を文字列で渡したとき
            var sw = Stopwatch.StartNew();
            for (int i = 0; i < COUNT; i++)
            {
                Requires.NotNull(key, "key");
            }
            sw.Stop();
            Console.WriteLine(
                "Requires.NotNull<T>(T value, string name) : {0} ms",
                sw.ElapsedMilliseconds);

            // 変数名を式木から取り出すとき
            sw.Reset();
            sw.Start();
            for (int i = 0; i < COUNT; i++)
            {
                Requires.NotNull(key, () => key);
            }
            sw.Stop();
            Console.WriteLine(
                "Requires.NotNull<T>(T value, Expression<Func<T>> lambda) : {0} ms",
                sw.ElapsedMilliseconds);

            // 式木の評価を遅延させたとき
            sw.Reset();
            sw.Start();
            for (int i = 0; i < COUNT; i++)
            {
                RequiresEx.NotNull(key, () => key);
            }
            sw.Stop();
            Console.WriteLine(
                "RequiresEx.NotNull<T>(T value, Expression<Func<T>> lambda) : {0} ms",
                sw.ElapsedMilliseconds);

            Console.ReadLine();
        }
    }
}

この結果がこちら。

Requires.NotNull<T>(T value, string name) : 2 ms
Requires.NotNull<T>(T value, Expression<Func<T>> lambda) : 664 ms
RequiresEx.NotNull<T>(T value, Expression<Func<T>> lambda) : 615 ms

式木から取り出す方は、変数名を文字列で渡した方に比べて、300倍以上遅い。式木の評価を遅延したとしても、ほんの少しの改善にしかならない。式木の構築は思っていた以上に高コストみたいだ。

コードの読みやすさとか、リファクタリングのしやすさを重視していたけど、パフォーマンスも気をつけるべきだったな。上記サンプルの場合、変数名を文字列で渡す方法でも、コードの品質は十分。むしろ、式木使うのはやりすぎか。反省。

皆さん、やりすぎにはくれぐれもご注意を。