Pertimbangkan manipulasi sederhana berikut atas koleksi:
static List<int> x = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var result = x.Where(i => i % 2 == 0).Where(i => i > 5);
Sekarang mari gunakan Ekspresi. Kode berikut kira-kira setara:
static void UsingLambda() {
Func<IEnumerable<int>, IEnumerable<int>> lambda = l => l.Where(i => i % 2 == 0).Where(i => i > 5);
var t0 = DateTime.Now.Ticks;
for (int j = 1; j < MAX; j++)
var sss = lambda(x).ToList();
var tn = DateTime.Now.Ticks;
Console.WriteLine("Using lambda: {0}", tn - t0);
}
Tapi saya ingin membangun ekspresi dengan cepat, jadi inilah tes baru:
static void UsingCompiledExpression() {
var f1 = (Expression<Func<IEnumerable<int>, IEnumerable<int>>>)(l => l.Where(i => i % 2 == 0));
var f2 = (Expression<Func<IEnumerable<int>, IEnumerable<int>>>)(l => l.Where(i => i > 5));
var argX = Expression.Parameter(typeof(IEnumerable<int>), "x");
var f3 = Expression.Invoke(f2, Expression.Invoke(f1, argX));
var f = Expression.Lambda<Func<IEnumerable<int>, IEnumerable<int>>>(f3, argX);
var c3 = f.Compile();
var t0 = DateTime.Now.Ticks;
for (int j = 1; j < MAX; j++)
var sss = c3(x).ToList();
var tn = DateTime.Now.Ticks;
Console.WriteLine("Using lambda compiled: {0}", tn - t0);
}
Tentu saja tidak persis seperti di atas, jadi agar adil, saya sedikit memodifikasi yang pertama:
static void UsingLambdaCombined() {
Func<IEnumerable<int>, IEnumerable<int>> f1 = l => l.Where(i => i % 2 == 0);
Func<IEnumerable<int>, IEnumerable<int>> f2 = l => l.Where(i => i > 5);
Func<IEnumerable<int>, IEnumerable<int>> lambdaCombined = l => f2(f1(l));
var t0 = DateTime.Now.Ticks;
for (int j = 1; j < MAX; j++)
var sss = lambdaCombined(x).ToList();
var tn = DateTime.Now.Ticks;
Console.WriteLine("Using lambda combined: {0}", tn - t0);
}
Sekarang sampai pada hasil untuk MAX = 100000, VS2008, debugging ON:
Using lambda compiled: 23437500
Using lambda: 1250000
Using lambda combined: 1406250
Dan dengan debugging OFF:
Using lambda compiled: 21718750
Using lambda: 937500
Using lambda combined: 1093750
Kejutan . Ekspresi yang dikompilasi kira-kira 17x lebih lambat dari alternatif lainnya. Sekarang inilah pertanyaannya:
- Apakah saya membandingkan ekspresi yang tidak setara?
- Apakah ada mekanisme untuk membuat .NET "mengoptimalkan" ekspresi yang dikompilasi?
- Bagaimana cara mengekspresikan panggilan berantai yang sama secara
l.Where(i => i % 2 == 0).Where(i => i > 5);
terprogram?
Beberapa statistik lagi. Visual Studio 2010, debugging ON, optimization OFF:
Using lambda: 1093974
Using lambda compiled: 15315636
Using lambda combined: 781410
Debugging AKTIF, pengoptimalan AKTIF:
Using lambda: 781305
Using lambda compiled: 15469839
Using lambda combined: 468783
Debugging NONAKTIF, pengoptimalan AKTIF:
Using lambda: 625020
Using lambda compiled: 14687970
Using lambda combined: 468765
Kejutan Baru. Beralih dari VS2008 (C # 3) ke VS2010 (C # 4), membuat UsingLambdaCombined
lebih cepat daripada lambda asli.
Oke, saya telah menemukan cara untuk meningkatkan kinerja yang dikompilasi lambda dengan lebih dari urutan besarnya. Berikut tipnya; setelah menjalankan profiler, 92% waktunya dihabiskan untuk:
System.Reflection.Emit.DynamicMethod.CreateDelegate(class System.Type, object)
Hmmmm ... Mengapa itu membuat delegasi baru di setiap iterasi? Saya tidak yakin, tetapi solusinya mengikuti di posting terpisah.
sumber
Stopwatch
untuk pengaturan waktu daripadaDateTime.Now
.Jawaban:
Mungkinkah lambda bagian dalam tidak dikompilasi?!? Inilah bukti konsepnya:
static void UsingCompiledExpressionWithMethodCall() { var where = typeof(Enumerable).GetMember("Where").First() as System.Reflection.MethodInfo; where = where.MakeGenericMethod(typeof(int)); var l = Expression.Parameter(typeof(IEnumerable<int>), "l"); var arg0 = Expression.Parameter(typeof(int), "i"); var lambda0 = Expression.Lambda<Func<int, bool>>( Expression.Equal(Expression.Modulo(arg0, Expression.Constant(2)), Expression.Constant(0)), arg0).Compile(); var c1 = Expression.Call(where, l, Expression.Constant(lambda0)); var arg1 = Expression.Parameter(typeof(int), "i"); var lambda1 = Expression.Lambda<Func<int, bool>>(Expression.GreaterThan(arg1, Expression.Constant(5)), arg1).Compile(); var c2 = Expression.Call(where, c1, Expression.Constant(lambda1)); var f = Expression.Lambda<Func<IEnumerable<int>, IEnumerable<int>>>(c2, l); var c3 = f.Compile(); var t0 = DateTime.Now.Ticks; for (int j = 1; j < MAX; j++) { var sss = c3(x).ToList(); } var tn = DateTime.Now.Ticks; Console.WriteLine("Using lambda compiled with MethodCall: {0}", tn - t0); }
Dan sekarang waktunya adalah:
Using lambda: 625020 Using lambda compiled: 14687970 Using lambda combined: 468765 Using lambda compiled with MethodCall: 468765
Woot! Tidak hanya cepat, ini lebih cepat dari lambda asli. ( Gores kepala ).
Tentu saja kode di atas terlalu menyakitkan untuk ditulis. Mari kita lakukan sihir sederhana:
static void UsingCompiledConstantExpressions() { var f1 = (Func<IEnumerable<int>, IEnumerable<int>>)(l => l.Where(i => i % 2 == 0)); var f2 = (Func<IEnumerable<int>, IEnumerable<int>>)(l => l.Where(i => i > 5)); var argX = Expression.Parameter(typeof(IEnumerable<int>), "x"); var f3 = Expression.Invoke(Expression.Constant(f2), Expression.Invoke(Expression.Constant(f1), argX)); var f = Expression.Lambda<Func<IEnumerable<int>, IEnumerable<int>>>(f3, argX); var c3 = f.Compile(); var t0 = DateTime.Now.Ticks; for (int j = 1; j < MAX; j++) { var sss = c3(x).ToList(); } var tn = DateTime.Now.Ticks; Console.WriteLine("Using lambda compiled constant: {0}", tn - t0); }
Dan beberapa pengaturan waktu, VS2010, Pengoptimalan AKTIF, Debugging MATI:
Using lambda: 781260 Using lambda compiled: 14687970 Using lambda combined: 468756 Using lambda compiled with MethodCall: 468756 Using lambda compiled constant: 468756
Sekarang Anda dapat berargumen bahwa saya tidak membuat keseluruhan ekspresi secara dinamis; hanya doa yang dirantai. Tetapi dalam contoh di atas saya menghasilkan ekspresi keseluruhan. Dan waktunya cocok. Ini hanyalah jalan pintas untuk menulis lebih sedikit kode.
Dari pemahaman saya, apa yang terjadi adalah bahwa metode .Compile () tidak menyebarkan kompilasi ke lambda dalam, dan dengan demikian pemanggilan konstan
CreateDelegate
. Tetapi untuk benar-benar memahami hal ini, saya ingin memiliki komentar guru .NET sedikit tentang hal-hal internal yang terjadi.Dan kenapa , oh kenapa sekarang ini lebih cepat dari lambda asli !?
sumber
Baru-baru ini saya mengajukan pertanyaan yang hampir identik:
Performa Ekspresi yang dikompilasi untuk didelegasikan
Solusi bagi saya adalah bahwa saya tidak harus menelepon
Compile
padaExpression
, tapi yang saya harus meneleponCompileToMethod
di atasnya dan mengkompilasiExpression
kestatic
metode dalam dinamis perakitan.Seperti:
var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly( new AssemblyName("MyAssembly_" + Guid.NewGuid().ToString("N")), AssemblyBuilderAccess.Run); var moduleBuilder = assemblyBuilder.DefineDynamicModule("Module"); var typeBuilder = moduleBuilder.DefineType("MyType_" + Guid.NewGuid().ToString("N"), TypeAttributes.Public)); var methodBuilder = typeBuilder.DefineMethod("MyMethod", MethodAttributes.Public | MethodAttributes.Static); expression.CompileToMethod(methodBuilder); var resultingType = typeBuilder.CreateType(); var function = Delegate.CreateDelegate(expression.Type, resultingType.GetMethod("MyMethod"));
Namun itu tidak ideal. Aku tidak cukup yakin yang jenis ini berlaku persis, tapi saya berpikir bahwa jenis yang diambil sebagai parameter oleh delegasi, atau dikembalikan oleh delegasi harus menjadi
public
dan non-generik. Ini harus non-generik karena tipe generik tampaknya aksesSystem.__Canon
yang merupakan tipe internal yang digunakan oleh .NET di bawah tenda untuk tipe generik dan ini melanggar aturan "harus menjadipublic
tipe).Untuk tipe tersebut, Anda bisa menggunakan yang tampaknya lebih lambat
Compile
. Saya mendeteksinya dengan cara berikut:private static bool IsPublicType(Type t) { if ((!t.IsPublic && !t.IsNestedPublic) || t.IsGenericType) { return false; } int lastIndex = t.FullName.LastIndexOf('+'); if (lastIndex > 0) { var containgTypeName = t.FullName.Substring(0, lastIndex); var containingType = Type.GetType(containgTypeName + "," + t.Assembly); if (containingType != null) { return containingType.IsPublic; } return false; } else { return t.IsPublic; } }
Tapi seperti yang saya katakan, ini tidak ideal dan saya masih ingin tahu mengapa mengompilasi metode ke perakitan dinamis terkadang urutan besarnya lebih cepat. Dan saya katakan kadang-kadang karena saya juga melihat kasus di mana
Expression
dikompilasi denganCompile
secepat metode normal. Lihat pertanyaan saya untuk itu.Atau jika seseorang mengetahui cara untuk melewati
public
batasan "tanpa non- tipe" dengan dynamic assembly, itu juga diterima.sumber
Ekspresi Anda tidak setara dan dengan demikian Anda mendapatkan hasil yang miring. Saya menulis bangku tes untuk menguji ini. Pengujian tersebut mencakup panggilan lambda reguler, ekspresi terkompilasi yang setara, ekspresi terkompilasi yang dibuat dengan tangan, serta versi yang disusun. Ini harus angka yang lebih akurat. Menariknya, saya tidak melihat banyak variasi antara versi biasa dan versi tersusun. Dan ekspresi yang dikompilasi lebih lambat secara alami tetapi hanya dengan sangat sedikit. Anda membutuhkan input dan jumlah iterasi yang cukup besar untuk mendapatkan beberapa angka yang bagus. Itu membuat perbedaan.
Adapun pertanyaan kedua Anda, saya tidak tahu bagaimana Anda bisa mendapatkan lebih banyak kinerja dari ini jadi saya tidak bisa membantu Anda di sana. Ini terlihat sebagus yang akan didapat.
Anda akan menemukan jawaban saya untuk pertanyaan ketiga Anda dalam
HandMadeLambdaExpression()
metode ini. Bukan ekspresi termudah untuk dibuat karena metode ekstensi, tetapi bisa dilakukan.using System; using System.Collections.Generic; using System.Linq; using System.Diagnostics; using System.Linq.Expressions; namespace ExpressionBench { class Program { static void Main(string[] args) { var values = Enumerable.Range(0, 5000); var lambda = GetLambda(); var lambdaExpression = GetLambdaExpression().Compile(); var handMadeLambdaExpression = GetHandMadeLambdaExpression().Compile(); var composed = GetComposed(); var composedExpression = GetComposedExpression().Compile(); var handMadeComposedExpression = GetHandMadeComposedExpression().Compile(); DoTest("Lambda", values, lambda); DoTest("Lambda Expression", values, lambdaExpression); DoTest("Hand Made Lambda Expression", values, handMadeLambdaExpression); Console.WriteLine(); DoTest("Composed", values, composed); DoTest("Composed Expression", values, composedExpression); DoTest("Hand Made Composed Expression", values, handMadeComposedExpression); } static void DoTest<TInput, TOutput>(string name, TInput sequence, Func<TInput, TOutput> operation, int count = 1000000) { for (int _ = 0; _ < 1000; _++) operation(sequence); var sw = Stopwatch.StartNew(); for (int _ = 0; _ < count; _++) operation(sequence); sw.Stop(); Console.WriteLine("{0}:", name); Console.WriteLine(" Elapsed: {0,10} {1,10} (ms)", sw.ElapsedTicks, sw.ElapsedMilliseconds); Console.WriteLine(" Average: {0,10} {1,10} (ms)", decimal.Divide(sw.ElapsedTicks, count), decimal.Divide(sw.ElapsedMilliseconds, count)); } static Func<IEnumerable<int>, IList<int>> GetLambda() { return v => v.Where(i => i % 2 == 0).Where(i => i > 5).ToList(); } static Expression<Func<IEnumerable<int>, IList<int>>> GetLambdaExpression() { return v => v.Where(i => i % 2 == 0).Where(i => i > 5).ToList(); } static Expression<Func<IEnumerable<int>, IList<int>>> GetHandMadeLambdaExpression() { var enumerableMethods = typeof(Enumerable).GetMethods(); var whereMethod = enumerableMethods .Where(m => m.Name == "Where") .Select(m => m.MakeGenericMethod(typeof(int))) .Where(m => m.GetParameters()[1].ParameterType == typeof(Func<int, bool>)) .Single(); var toListMethod = enumerableMethods .Where(m => m.Name == "ToList") .Select(m => m.MakeGenericMethod(typeof(int))) .Single(); // helpers to create the static method call expressions Func<Expression, ParameterExpression, Func<ParameterExpression, Expression>, Expression> WhereExpression = (instance, param, body) => Expression.Call(whereMethod, instance, Expression.Lambda(body(param), param)); Func<Expression, Expression> ToListExpression = instance => Expression.Call(toListMethod, instance); //return v => v.Where(i => i % 2 == 0).Where(i => i > 5).ToList(); var exprParam = Expression.Parameter(typeof(IEnumerable<int>), "v"); var expr0 = WhereExpression(exprParam, Expression.Parameter(typeof(int), "i"), i => Expression.Equal(Expression.Modulo(i, Expression.Constant(2)), Expression.Constant(0))); var expr1 = WhereExpression(expr0, Expression.Parameter(typeof(int), "i"), i => Expression.GreaterThan(i, Expression.Constant(5))); var exprBody = ToListExpression(expr1); return Expression.Lambda<Func<IEnumerable<int>, IList<int>>>(exprBody, exprParam); } static Func<IEnumerable<int>, IList<int>> GetComposed() { Func<IEnumerable<int>, IEnumerable<int>> composed0 = v => v.Where(i => i % 2 == 0); Func<IEnumerable<int>, IEnumerable<int>> composed1 = v => v.Where(i => i > 5); Func<IEnumerable<int>, IList<int>> composed2 = v => v.ToList(); return v => composed2(composed1(composed0(v))); } static Expression<Func<IEnumerable<int>, IList<int>>> GetComposedExpression() { Expression<Func<IEnumerable<int>, IEnumerable<int>>> composed0 = v => v.Where(i => i % 2 == 0); Expression<Func<IEnumerable<int>, IEnumerable<int>>> composed1 = v => v.Where(i => i > 5); Expression<Func<IEnumerable<int>, IList<int>>> composed2 = v => v.ToList(); var exprParam = Expression.Parameter(typeof(IEnumerable<int>), "v"); var exprBody = Expression.Invoke(composed2, Expression.Invoke(composed1, Expression.Invoke(composed0, exprParam))); return Expression.Lambda<Func<IEnumerable<int>, IList<int>>>(exprBody, exprParam); } static Expression<Func<IEnumerable<int>, IList<int>>> GetHandMadeComposedExpression() { var enumerableMethods = typeof(Enumerable).GetMethods(); var whereMethod = enumerableMethods .Where(m => m.Name == "Where") .Select(m => m.MakeGenericMethod(typeof(int))) .Where(m => m.GetParameters()[1].ParameterType == typeof(Func<int, bool>)) .Single(); var toListMethod = enumerableMethods .Where(m => m.Name == "ToList") .Select(m => m.MakeGenericMethod(typeof(int))) .Single(); Func<ParameterExpression, Func<ParameterExpression, Expression>, Expression> LambdaExpression = (param, body) => Expression.Lambda(body(param), param); Func<Expression, ParameterExpression, Func<ParameterExpression, Expression>, Expression> WhereExpression = (instance, param, body) => Expression.Call(whereMethod, instance, Expression.Lambda(body(param), param)); Func<Expression, Expression> ToListExpression = instance => Expression.Call(toListMethod, instance); var composed0 = LambdaExpression(Expression.Parameter(typeof(IEnumerable<int>), "v"), v => WhereExpression( v, Expression.Parameter(typeof(int), "i"), i => Expression.Equal(Expression.Modulo(i, Expression.Constant(2)), Expression.Constant(0)))); var composed1 = LambdaExpression(Expression.Parameter(typeof(IEnumerable<int>), "v"), v => WhereExpression( v, Expression.Parameter(typeof(int), "i"), i => Expression.GreaterThan(i, Expression.Constant(5)))); var composed2 = LambdaExpression(Expression.Parameter(typeof(IEnumerable<int>), "v"), v => ToListExpression(v)); var exprParam = Expression.Parameter(typeof(IEnumerable<int>), "v"); var exprBody = Expression.Invoke(composed2, Expression.Invoke(composed1, Expression.Invoke(composed0, exprParam))); return Expression.Lambda<Func<IEnumerable<int>, IList<int>>>(exprBody, exprParam); } } }
Dan hasilnya di mesin saya:
sumber
Performa lambda yang dikompilasi atas delegasi mungkin lebih lambat karena kode yang dikompilasi pada waktu proses mungkin tidak dioptimalkan namun kode yang Anda tulis secara manual dan yang dikompilasi melalui kompiler C # dioptimalkan.
Kedua, beberapa ekspresi lambda berarti beberapa metode anonim, dan memanggil masing-masing membutuhkan sedikit waktu ekstra untuk mengevaluasi metode lurus. Misalnya, menelepon
dan
Action x => Console.WriteLine(x); x(); // this means two different calls..
berbeda, dan dengan satu detik lebih sedikit overhead diperlukan karena dari perspektif penyusun, sebenarnya dua panggilan berbeda. Pertama-tama panggil x itu sendiri dan kemudian di dalam pemanggilan pernyataan x itu.
Jadi Lambda gabungan Anda pasti akan memiliki kinerja yang sedikit lambat dibandingkan ekspresi lambda tunggal.
Dan ini tidak tergantung pada apa yang dijalankan di dalamnya, karena Anda masih mengevaluasi logika yang benar, tetapi Anda menambahkan langkah-langkah tambahan untuk dijalankan oleh compiler.
Bahkan setelah pohon ekspresi dikompilasi, ia tidak akan memiliki pengoptimalan, dan ia masih akan mempertahankan struktur kecilnya yang rumit, mengevaluasi dan memanggilnya mungkin memiliki validasi tambahan, pemeriksaan null, dll. Yang mungkin memperlambat kinerja ekspresi lambda yang dikompilasi.
sumber
UsingLambdaCombined
pengujian ini menggabungkan beberapa fungsi lambda, dan kinerjanya sangat mendekatiUsingLambda
. Mengenai pengoptimalan, saya yakin bahwa mereka ditangani oleh mesin JIT, dan dengan demikian kode yang dihasilkan waktu proses (setelah kompilasi), juga akan menjadi target pengoptimalan JIT apa pun.