デリゲートのパフォーマンス

  • 50
    Like
  • 0
    Comment
More than 1 year has passed since last update.

デリゲートを受け取るメソッドは、メソッドのシグネチャさえ一致していれば、下記のどちらの記述も受け取ることができる。

A
.Where( x => string.IsNullOrWhiteSpace( x ) );
B
.Where( string.IsNullOrWhiteSpace );

検証したところ、Aのパターンが圧倒的に効率良く、Bは、効率が悪かった。

検証コード
using System;
using System.Diagnostics;
namespace TestSpace {
    class Program {
        static void Main( string[] args ) {
            var count = 100000000;
            var sw = new Stopwatch();

            // --------- A
            var c = 0;
            sw.Start();
            for( int i = 0; i < count; i++ ) {
                if( Where( x => string.IsNullOrWhiteSpace( x ) , "test" ) )
                    c++;
            }
            sw.Stop();

            Console.WriteLine( $"A {sw.Elapsed.ToString()} GC:{GCCount.ToString()} Mem:{GC.GetTotalMemory( false )}" );

            // -------- B

            c = 0;
            sw.Restart();
            for( int i = 0; i < count; i++ ) {
                if( Where( string.IsNullOrWhiteSpace , "test" ) )
                    c++;
            }
            sw.Stop();
            Console.WriteLine( $"B {sw.Elapsed.ToString()} GC:{GCCount.ToString()} Mem:{GC.GetTotalMemory( false )}" );
            Console.ReadLine();
        }

        static bool Where( Func<string , bool> call , string x ) => call( x );
        static int GCCount => GC.CollectionCount( 0 ) + GC.CollectionCount( 1 ) + GC.CollectionCount( 2 );
    }
}
検証結果
A 00:00:01.7168550 GC:0 Mem:29836
B 00:00:02.8687679 GC:1525 Mem:1850412

パターンAが完了した時点では、GCが発生しておらず、マネージメモリもほとんど消費していない。
一方、パターンBは、GCが頻繁に発生しており、マネージメモリを比較的多く消費している。

コンパイル後のコード

using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace TestSpace
{
    internal class Program
    {
        [CompilerGenerated]
        [Serializable]
        private sealed class <>c
        {
            public static readonly Program.<>c <>9 = new Program.<>c();
            public static Func<string, bool> <>9__0_0;
            internal bool <Main>b__0_0(string x)
            {
                return string.IsNullOrWhiteSpace(x);
            }
        }
        private static int GCCount
        {
            get
            {
                return GC.CollectionCount(0) + GC.CollectionCount(1) + GC.CollectionCount(2);
            }
        }
        private static void Main(string[] args)
        {
            int num = 100000000;
            Stopwatch stopwatch = new Stopwatch();
            int num2 = 0;
            stopwatch.Start();
            for (int i = 0; i < num; i++)
            {
                Func<string, bool> arg_3C_0;
                if ((arg_3C_0 = Program.<>c.<>9__0_0) == null)
                {
                    arg_3C_0 = (Program.<>c.<>9__0_0 = new Func<string, bool>(Program.<>c.<>9.<Main>b__0_0));
                }
                if (Program.Where(arg_3C_0, "test"))
                {
                    num2++;
                }
            }
            stopwatch.Stop();
            Console.WriteLine(string.Format("A {0} GC:{1} Mem:{2}", stopwatch.Elapsed.ToString(), Program.GCCount.ToString(), GC.GetTotalMemory(false)));
            num2 = 0;
            stopwatch.Restart();
            for (int j = 0; j < num; j++)
            {
                if (Program.Where(new Func<string, bool>(string.IsNullOrWhiteSpace), "test"))
                {
                    num2++;
                }
            }
            stopwatch.Stop();
            Console.WriteLine(string.Format("B {0} GC:{1} Mem:{2}", stopwatch.Elapsed.ToString(), Program.GCCount.ToString(), GC.GetTotalMemory(false)));
            Console.ReadLine();
        }
        private static bool Where(Func<string, bool> call, string x)
        {
            return call(x);
        }
    }
}

パターンAは、初回時にデリゲートをキャッシュしているが、パターンBでは、毎回、デリゲートを生成している。

更新履歴

2015/07/14 : 2回目の計測でRestartされていなかったので修正。