24

via .Compile()Func<>から作成されたものは、宣言されたものを直接使用するよりもかなり遅いのはなぜですか?Expression<Func<>>Func<>

Func<IInterface, object>宣言されたものを直接使用することから、Expression<Func<IInterface, object>>作業中のアプリで作成されたものに変更したところ、パフォーマンスが低下したことに気付きました。

ちょっとしたテストを行ったところ、式からの作成には、直接宣言されFunc<>た場合の「ほぼ」2倍の時間がかかります。Func<>

私のマシンでは、ダイレクトFunc<>に約 7.5 秒、ダイレクトにExpression<Func<>>約 12.6 秒かかります。

これが私が使用したテストコードです(Net 4.0を実行)

// Direct
Func<int, Foo> test1 = x => new Foo(x * 2);

int counter1 = 0;

Stopwatch s1 = new Stopwatch();
s1.Start();
for (int i = 0; i < 300000000; i++)
{
 counter1 += test1(i).Value;
}
s1.Stop();
var result1 = s1.Elapsed;



// Expression . Compile()
Expression<Func<int, Foo>> expression = x => new Foo(x * 2);
Func<int, Foo> test2 = expression.Compile();

int counter2 = 0;

Stopwatch s2 = new Stopwatch();
s2.Start();
for (int i = 0; i < 300000000; i++)
{
 counter2 += test2(i).Value;
}
s2.Stop();
var result2 = s2.Elapsed;



public class Foo
{
 public Foo(int i)
 {
  Value = i;
 }
 public int Value { get; set; }
}

どうすればパフォーマンスを取り戻すことができますか?

Func<>から作成されたExpression<Func<>>ものを直接宣言されたものと同じように実行するためにできることはありますか?

4

6 に答える 6

19

他の人が述べたように、動的デリゲートを呼び出すオーバーヘッドが速度低下の原因となっています。私のコンピューターでは、CPU が 3 GHz の場合、そのオーバーヘッドは約 12ns です。これを回避する方法は、次のようにコンパイル済みアセンブリからメソッドをロードすることです。

var ab = AppDomain.CurrentDomain.DefineDynamicAssembly(
             new AssemblyName("assembly"), AssemblyBuilderAccess.Run);
var mod = ab.DefineDynamicModule("module");
var tb = mod.DefineType("type", TypeAttributes.Public);
var mb = tb.DefineMethod(
             "test3", MethodAttributes.Public | MethodAttributes.Static);
expression.CompileToMethod(mb);
var t = tb.CreateType();
var test3 = (Func<int, Foo>)Delegate.CreateDelegate(
                typeof(Func<int, Foo>), t.GetMethod("test3"));

int counter3 = 0;
Stopwatch s3 = new Stopwatch();
s3.Start();
for (int i = 0; i < 300000000; i++)
{
    counter3 += test3(i).Value;
}
s3.Stop();
var result3 = s3.Elapsed;

上記のコードを追加すると、約 1ns のオーバーヘッドで、result3は常に よりわずか 1 秒程度高くなります。result1

test2では、より高速なデリゲート ( ) を使用できるのに、コンパイルされたラムダ ( ) をわざわざ使う必要があるtest3でしょうか。動的アセンブリを作成すると、一般にオーバーヘッドがはるかに大きくなり、呼び出しごとに 10 ~ 20 ナノ秒しか節約できないためです。

于 2010-11-18T06:06:44.580 に答える
6

(これは適切な回答ではありませんが、回答を発見するのに役立つ資料です。)

Mono 2.6.7 - Debian Lenny - Linux 2.6.26 i686 - 2.80GHz シングル コアから収集された統計:

      Func: 00:00:23.6062578
Expression: 00:00:23.9766248

したがって、Mono では、少なくとも両方のメカニズムが同等の IL を生成するように見えます。

gmcsこれは、無名メソッド用にMono によって生成された ILです。

// method line 6
.method private static  hidebysig
       default class Foo '<Main>m__0' (int32 x)  cil managed
{
    .custom instance void class [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::'.ctor'() =  (01 00 00 00 ) // ....

    // Method begins at RVA 0x2204
    // Code size 9 (0x9)
    .maxstack 8
    IL_0000:  ldarg.0
    IL_0001:  ldc.i4.2
    IL_0002:  mul
    IL_0003:  newobj instance void class Foo::'.ctor'(int32)
    IL_0008:  ret
} // end of method Default::<Main>m__0

式コンパイラによって生成された IL の抽出に取り組みます。

于 2010-11-18T04:15:59.970 に答える
4

最終的にはExpression<T>、コンパイル済みのデリゲートではないということです。ただの表現ツリーです。LambdaExpression(実際には)でCompileを呼び出すExpression<T>と、実行時にILコードが生成され、それに類似したものが作成されますDynamicMethod

inコードを使用する場合はFunc<T>、他のデリゲート参照と同じようにプリコンパイルします。

したがって、ここには2つの速度低下の原因があります。

  1. Expression<T>デリゲートにコンパイルするための最初のコンパイル時間。これは巨大です。呼び出しごとにこれを行う場合は、絶対に行わないでください(ただし、コンパイルを呼び出した後にストップウォッチを使用しているため、そうではありません。

  2. DynamicMethod基本的には、コンパイルを呼び出した後です。DynamicMethod■(強く型付けされたデリゲートでさえ)実際には、直接呼び出しよりも実行が遅くなります。Func<T>コンパイル時に解決されるのは直接呼び出しです。動的に放出されるILとコンパイル時に放出されるILの間には、パフォーマンスの比較があります。ランダムURL:http ://www.codeproject.com/KB/cs/dynamicmethoddelegates.aspx?msg=1160046

...また、のストップウォッチテストではExpression<T>、0ではなくi = 1のときにタイマーを開始する必要があります...コンパイルされたLambdaは最初の呼び出しまでJITコンパイルされないため、パフォーマンスが低下します。その最初の呼び出し。

于 2010-11-18T04:48:15.913 に答える
1

これは、コードの最初の呼び出しが変更されなかったことが原因である可能性があります。私はILを見ることにしました、そしてそれらは事実上同一です。

Func<int, Foo> func = x => new Foo(x * 2);
Expression<Func<int, Foo>> exp = x => new Foo(x * 2);
var func2 = exp.Compile();
Array.ForEach(func.Method.GetMethodBody().GetILAsByteArray(), b => Console.WriteLine(b));

var mtype = func2.Method.GetType();
var fiOwner = mtype.GetField("m_owner", BindingFlags.Instance | BindingFlags.NonPublic);
var dynMethod = fiOwner.GetValue(func2.Method) as DynamicMethod;
var ilgen = dynMethod.GetILGenerator();


byte[] il = ilgen.GetType().GetMethod("BakeByteArray", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(ilgen, null) as byte[];
Console.WriteLine("Expression version");
Array.ForEach(il, b => Console.WriteLine(b));

このコードはバイト配列を取得し、それらをコンソールに出力します。これが私のマシンの出力です::

2
24
90
115
13
0
0
6
42
Expression version
3
24
90
115
2
0
0
6
42

そして、これが最初の関数のリフレクターのバージョンです::

   L_0000: ldarg.0 
    L_0001: ldc.i4.2 
    L_0002: mul 
    L_0003: newobj instance void ConsoleApplication7.Foo::.ctor(int32)
    L_0008: ret 

メソッド全体で異なるのは2バイトだけです!これらは最初のオペコードであり、最初のメソッドldarg0(最初の引数をロード)用ですが、2番目のメソッドldarg1(2番目の引数をロード)用です。ここでの違いは、式で生成されたオブジェクトには実際にはオブジェクトのターゲットがあるためClosureです。これも考慮に入れることができます。

両方の次のオペコードはldc.i4.2(24)で、これはスタックに2をロードすることを意味し、次はmul(90)のオペコード、次のオペコードはnewobj(115)のオペコードです。次の4バイトは、.ctorオブジェクトのメタデータトークンです。2つのメソッドは実際には異なるアセンブリでホストされているため、これらは異なります。匿名メソッドは匿名アセンブリにあります。残念ながら、私はこれらのトークンを解決する方法を理解するまでには至っていません。最終的なオペコードは42で、これはretです。すべてのCLI関数はret、何も返さない関数で終了する必要があります。

可能性はほとんどありません。クロージャーオブジェクトが原因で速度が低下している可能性があります。これは本当かもしれませんが(可能性は低いですが)、ジッターはメソッドを妨害しませんでした。そのパスをジッターし、より遅いパスを呼び出します。vsのC#コンパイラは、さまざまな呼び出し規約MethodAttributesを発行している可能性があり、さまざまな最適化を実行するためのジッターへのヒ​​ントとして機能する可能性があります。

最終的には、この違いについてリモートで心配することすらありません。アプリケーションの過程で実際に関数を30億回呼び出していて、発生する差が5秒である場合は、おそらく問題ないでしょう。

于 2010-11-18T05:04:31.937 に答える
1

記録のために: 上記のコードで数値を再現できます。

注意すべきことの 1 つは、両方のデリゲートが反復ごとに Foo の新しいインスタンスを作成することです。これは、デリゲートの作成方法よりも重要になる可能性があります。これは大量のヒープ割り当てにつながるだけでなく、GC もこの数値に影響を与える可能性があります。

コードを次のように変更すると

Func<int, int> test1 = x => x * 2;

Expression<Func<int, int>> expression = x => x * 2;
Func<int, int> test2 = expression.Compile();

パフォーマンスの数値は実質的に同じです (実際には、結果 2 は結果 1 よりも少し優れています)。これは、高価な部分はヒープ割り当てやコレクションであり、デリゲートの構築方法ではないという理論をサポートしています。

アップデート

Gabe さんのコメントに従って、Foo構造体に変更してみました。残念ながら、これにより元のコードとほぼ同じ数値が得られるため、ヒープ割り当て/ガベージ コレクションが原因ではない可能性があります。

ただし、このタイプのデリゲートの数値も確認したFunc<int, int>ところ、元のコードの数値と非常に似ており、はるかに低くなっています。

私は掘り下げ続け、より多くの/更新された回答を見ることを楽しみにしています.

于 2010-11-18T06:56:13.513 に答える
0

Michael B. の回答に興味があったので、ストップウォッチが始まる前に、それぞれの場合に余分な呼び出しを追加しました。デバッグ モードでは、コンパイル (ケース 2) メソッドはほぼ 2 倍 (6 秒から 10 秒) 速くなり、リリース モードでは、両方のバージョンが同等でした (差は約 0.2 秒でした)。

さて、私が驚くべきことは、JIT を方程式から外すと、Martin とは反対の結果が得られたことです。

編集: 最初は Foo を見逃していたので、上記の結果はプロパティではなくフィールドを持つ Foo の結果であり、元の Foo との比較は同じで、時間だけが大きくなっています - 直接関数の場合は 15 秒、コンパイル済みバージョンの場合は 12 秒です。繰り返しますが、リリース モードでは時間は似ていますが、現在の差は約 0.5 です。

ただし、これは、式がより複雑な場合、リリース モードでも実際の違いがあることを示しています。

于 2010-11-18T06:34:56.907 に答える