拡張メソッドでboolにNot()を生やすとどの程度パフォーマンスに影響があるのか調べてみた

C#
このエントリーをはてなブックマークに追加

はい、昨日書いた記事(if(flag == true)はありなのか?なしなのか?)で気になったので調査してみました。

ベンチマークには前々から使ってみたかったBenchmarkDotNetを使います。

同じプログラムの相対比較なのでマシンスペックには影響されないと思いますが、いちおうマシンスペック:

  • Windows 10 pro
  • core i7 6700
  • GTX970
  • RAM 16GB

比較したいものはこちら

public static class BoolExtension
{
    public static bool Not(this bool b)
    {
        return !b;
    }
}

// ~~~~

// こいつらを比較したい
if(!flag){}

// おそらくこっちは最適化されずにほんの少しパフォーマンス悪いはず
if(flag.Not()){}

はい、そうです答えはもう予測で来てるんですよね。拡張メソッドNot()は最適化されずにメソッド呼び出しのオーバーヘッドでほんの少しパフォーマンス(速度)悪いと。 それで、知りたいのはどれほどパフォーマンスに影響するか、です。

計測コード貼っておきます。

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Attributes.Jobs;
using BenchmarkDotNet.Running;

namespace BoolBenchmark
{
    public static class BoolExtension
    {
        public static bool Not(this bool b)
        {
            return !b;
        }
    }

    [ClrJob]
    public class Program
    {
        static void Main(string[] args)
        {
            BenchmarkRunner.Run();
        }

        private bool canNotCount = false;

        [Benchmark]
        public ushort Not()
        {
            ushort count = 0;
            for (ushort i = 0; i < ushort.MaxValue; i++)
            {
                if (!canNotCount)
                {
                    count++;
                }
            }
            return count;
        }

        [Benchmark]
        public ushort NotExtension()
        {
            ushort count = 0;
            for (ushort i = 0; i < ushort.MaxValue; i++)
            {
                if (canNotCount.Not())
                {
                    count++;
                }
            }
            return count;
        }
    }
}

ループ回数がushort個数回なのは許してください、uint個数回にしたら長すぎたので。

そして、結果がこちら。

あまり変わらない。。。むしろ拡張メソッドのほうが(誤差の範囲かと思いますが)微妙に早い。。。。

これはプログラムの他の箇所の最適化を疑ってみる案件かと思いますので、ILSpyで逆コンパイルして中間言語を見てみることにします。

Notメソッドforループ:

NotExtensionメソッドforループ:

むむむ。。。変な最適化はされていないようです。

いちおうBenchmarkのログを見ることにします。

// * Detailed results *
Program.Not: Clr(Runtime=Clr)
Runtime = .NET Framework 4.6.1 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.2633.0; GC = Concurrent Workstation
Mean = 43.3868 us, StdErr = 0.2119 us (0.49%); N = 17, StdDev = 0.8735 us
Min = 42.2022 us, Q1 = 42.7483 us, Median = 43.2139 us, Q3 = 44.0902 us, Max = 45.2624 us
IQR = 1.3419 us, LowerFence = 40.7354 us, UpperFence = 46.1031 us
ConfidenceInterval = [42.5362 us; 44.2374 us] (CI 99.9%), Margin = 0.8506 us (1.96% of Mean)
Skewness = 0.6, Kurtosis = 2.17


Program.NotExtension: Clr(Runtime=Clr)
Runtime = .NET Framework 4.6.1 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.2633.0; GC = Concurrent Workstation
Mean = 43.2029 us, StdErr = 0.0765 us (0.18%); N = 13, StdDev = 0.2759 us
Min = 42.7954 us, Q1 = 42.9566 us, Median = 43.2103 us, Q3 = 43.4135 us, Max = 43.7556 us
IQR = 0.4570 us, LowerFence = 42.2711 us, UpperFence = 44.0990 us
ConfidenceInterval = [42.8725 us; 43.5332 us] (CI 99.9%), Margin = 0.3304 us (0.76% of Mean)
Skewness = 0.27, Kurtosis = 1.96


Total time: 00:00:36 (36.81 sec)

// * Summary *

BenchmarkDotNet=v0.10.12, OS=Windows 10 Redstone 3 [1709, Fall Creators Update] (10.0.16299.248)
Intel Core i7-6700 CPU 3.40GHz (Skylake), 1 CPU, 8 logical cores and 4 physical cores
Frequency=3328126 Hz, Resolution=300.4694 ns, Timer=TSC
  [Host] : .NET Framework 4.6.1 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.2633.0
  Clr    : .NET Framework 4.6.1 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.2633.0

Job=Clr  Runtime=Clr  

       Method |     Mean |     Error |    StdDev |
------------- |---------:|----------:|----------:|
          Not | 43.39 us | 0.8506 us | 0.8735 us |
 NotExtension | 43.20 us | 0.3304 us | 0.2759 us |

// * Hints *
Outliers
  Program.NotExtension: Clr -> 2 outliers were removed

// * Legends *
  Mean   : Arithmetic mean of all measurements
  Error  : Half of 99.9% confidence interval
  StdDev : Standard deviation of all measurements
  1 us   : 1 Microsecond (0.000001 sec)

// ***** BenchmarkRunner: End *****
// * Artifacts cleanup *

HintのところにClr -> 2 outliers were removedとありますが、よくわからない。。。。

結論

それなりの使用回数では誤差の範囲内のパフォーマンス影響なので、コアロジック以外では使っても良さそう。

このあたりのこと詳しい人いたらぜひご教授くださいm(__)m

追記:ツッコミどころとしてビルド(debug or release)どっちだよとありますが、BenchmarkDotNet先輩がdebugビルドを許してくれなかったのでreleaseビルドです。

追記の追記:VSのプロファイラーを使ってみた

プログラム変更点:ushort→uint、BenchmarkDotNetを使わずに交互に3回呼び出し

かなり回せば若干のパフォーマンス差が出るようですね。予測通りでよかったよかった。

追記の追記の追記: よーく考えればこの手の最適化はJIT時ということに気づいてしまった