web-dev-qa-db-ja.com

式ツリーのパフォーマンス

私の現在の理解は、次のような「ハードコードされた」コードです。

public int Add(int x, int y) {return x + y;}

次のような式ツリーコードよりも常にパフォーマンスが向上します。

Expression<Func<int, int, int>> add = (x, y) => x + y; 
var result = add.Compile()(2, 3);

var x = Expression.Parameter(typeof(int)); 
var y = Expression.Parameter(typeof(int)); 
return (Expression.Lambda(Expression.Add(x, y), x, y).
    Compile() as Func<int, int, int>)(2, 3);

コンパイラーはより多くの情報を持っており、コンパイル時にコードをコンパイルすると、コードの最適化により多くの労力を費やす可能性があるためです。これは一般的に本当ですか?

23
cs0815

コンパイル

Expression.Compileの呼び出しは、次の意味で、アプリケーションに含まれる他の.NETコードとまったく同じプロセスを経ます。

  • ILコードが生成されます
  • ILコードはマシンコードにJITされています

(式ツリーが既に作成されており、入力コードから生成する必要がないため、解析ステップはスキップされます)

式コンパイラの ソースコード を調べて、実際にILコードが生成されていることを確認できます。

最適化

CLRによって行われる最適化のほとんどすべては、C#ソースコードのコンパイルからではなく、JITステップで行われることに注意してください。この最適化は、ラムダデリゲートからマシンコードにILコードをコンパイルするときにも実行されます。

あなたの例

あなたの例では、リンゴとオレンジを比較しています。最初の例はメソッド定義であり、2番目の例はメソッドを作成し、コンパイルして実行するランタイムコードです。メソッドの作成/コンパイルにかかる時間は、実際に実行するよりもはるかに長くなります。ただし、作成後にコンパイル済みメソッドのインスタンスを保持できます。これを行うと、生成されたメソッドのパフォーマンスは元のC#メソッドのパフォーマンスと同じになります。

この場合を考えてみましょう:

private static int AddMethod(int a, int b)
{
    return a + b;
}

Func<int, int, int> add1 = (a, b) => a + b;
Func<int, int, int> add2 = AddMethod;

var x = Expression.Parameter(typeof (int));
var y = Expression.Parameter(typeof (int));
var additionExpr = Expression.Add(x, y);
Func<int, int, int> add3 = 
              Expression.Lambda<Func<int, int, int>>(
                  additionExpr, x, y).Compile();
//the above steps cost a lot of time, relatively.

//performance of these three should be identical
add1(1, 2);
add2(1, 2);
add3(1, 2);

したがって、結論としては、ILコードはどのように生成されてもILコードであり、Linq式はILコードを生成します。

20
Bas

Add関数は、おそらく何らかの関数オーバーヘッド(インライン化されていない場合)と単一の追加命令にコンパイルされます。それより速くなることはありません。

この式ツリーを構築することでさえ、桁違いに遅くなります。呼び出しごとに新しい関数をコンパイルすると、直接のC#実装と比較して非常にコストがかかります。

関数を一度だけコンパイルして、どこかに保存してみてください。

5
usr

ビルドとコンパイルされたラムダの実行が「デリゲート」よりもわずかに遅い理由を理解しようとしています(新しいSO質問)を作成する必要があると思います)このスレッドを見つけて確認することにしましたBenchmarkDotNetを使用したパフォーマンス。驚いたことに、手動でビルドし、コンパイルしたラムダが最も高速です。もちろん、メソッド間には安定した違いがあります。

結果:

BenchmarkDotNet=v0.10.5, OS=Windows 10.0.14393
Processor=Intel Core i5-2500K CPU 3.30GHz (Sandy Bridge), ProcessorCount=4
Frequency=3233539 Hz, Resolution=309.2587 ns, Timer=TSC
  [Host] : Clr 4.0.30319.42000, 64bit RyuJIT-v4.6.1648.0
  Clr    : Clr 4.0.30319.42000, 64bit RyuJIT-v4.6.1648.0
  Core   : .NET Core 4.6.25009.03, 64bit RyuJIT


         Method |  Job | Runtime |      Mean |     Error |    StdDev |    Median |       Min |        Max | Rank | Allocated |
--------------- |----- |-------- |----------:|----------:|----------:|----------:|----------:|-----------:|-----:|----------:|
     AddBuilded |  Clr |     Clr | 0.8826 ns | 0.0278 ns | 0.0232 ns | 0.8913 ns | 0.8429 ns |  0.9195 ns |    1 |       0 B |
      AddLambda |  Clr |     Clr | 1.5077 ns | 0.0226 ns | 0.0212 ns | 1.4986 ns | 1.4769 ns |  1.5395 ns |    2 |       0 B |
 AddLambdaConst |  Clr |     Clr | 6.4535 ns | 0.0454 ns | 0.0425 ns | 6.4439 ns | 6.4030 ns |  6.5323 ns |    3 |       0 B |
     AddBuilded | Core |    Core | 0.8993 ns | 0.0249 ns | 0.0233 ns | 0.8908 ns | 0.8777 ns |  0.9506 ns |    1 |       0 B |
      AddLambda | Core |    Core | 1.5105 ns | 0.0241 ns | 0.0201 ns | 1.5094 ns | 1.4731 ns |  1.5396 ns |    2 |       0 B |
 AddLambdaConst | Core |    Core | 9.3849 ns | 0.2237 ns | 0.5693 ns | 9.6577 ns | 8.3455 ns | 10.0590 ns |    4 |       0 B |

これから結論を出すことはできません。ILコードの違いやJITコンパイラの影響である可能性があります。

コード:

    static BenchmarkLambdaSimple()
    {
        addLambda = (a, b) => a + b;
        addLambdaConst = AddMethod;

        var x = Expression.Parameter(typeof(int));
        var y = Expression.Parameter(typeof(int));
        var additionExpr = Expression.Add(x, y);
        addBuilded =
                      Expression.Lambda<Func<int, int, int>>(
                          additionExpr, x, y).Compile();
    }
    static Func<int, int, int> addLambda;
    static Func<int, int, int> addLambdaConst;
    static Func<int, int, int> addBuilded;
    private static int AddMethod(int a, int b)
    {
        return a + b;
    }

    [Benchmark]
    public int AddBuilded()
    {
        return addBuilded(1, 2);
    }

    [Benchmark]
    public int AddLambda()
    {
        return addLambda(1, 2);
    }

    [Benchmark]
    public int AddLambdaConst()
    {
        return addLambdaConst(1, 2);
    }
3

OK、私は少しテストを書きました(おそらくあなたの専門家による精査が必要です)が、式ツリーが最も速く(add3)、次にadd2、次にadd1のようです!

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

namespace ExpressionTreeTest
{
    class Program
    {
        static void Main()
        {
            Func<int, int, int> add1 = (a, b) => a + b;
            Func<int, int, int> add2 = AddMethod;

            var x = Expression.Parameter(typeof(int));
            var y = Expression.Parameter(typeof(int));
            var additionExpr = Expression.Add(x, y);
            var add3 = Expression.Lambda<Func<int, int, int>>(
                              additionExpr, x, y).Compile();


            TimingTest(add1, "add1", 1000000);
            TimingTest(add2, "add2", 1000000);
            TimingTest(add3, "add3", 1000000);
        }

        private static void TimingTest(Func<int, int, int> addMethod, string testMethod, int numberOfAdditions)
        {
            var sw = new Stopwatch();
            sw.Start();
            for (var c = 0; c < numberOfAdditions; c++)
            {
               addMethod(1, 2);              
            }
            sw.Stop();
           Console.WriteLine("Total calculation time {1}: {0}", sw.Elapsed, testMethod);
        }

        private static int AddMethod(int a, int b)
        {
            return a + b;
        }
    }
}

私の結果のデバッグモード:

Total calculation time add1: 00:00:00.0134612
Total calculation time add2: 00:00:00.0133916
Total calculation time add3: 00:00:00.0053629

私の結果リリースモード:

Total calculation time add1: 00:00:00.0026172
Total calculation time add2: 00:00:00.0027046
Total calculation time add3: 00:00:00.0014334
2
cs0815