Saya punya pertanyaan tentang kinerja dynamic
dalam C #. Saya sudah baca dynamic
membuat kompiler berjalan lagi, tetapi apa fungsinya?
Apakah harus mengkompilasi ulang seluruh metode dengan dynamic
variabel yang digunakan sebagai parameter atau hanya garis-garis dengan perilaku / konteks dinamis?
Saya perhatikan bahwa menggunakan dynamic
variabel dapat memperlambat simpel untuk 2 kali lipat.
Kode yang saya mainkan:
internal class Sum2
{
public int intSum;
}
internal class Sum
{
public dynamic DynSum;
public int intSum;
}
class Program
{
private const int ITERATIONS = 1000000;
static void Main(string[] args)
{
var stopwatch = new Stopwatch();
dynamic param = new Object();
DynamicSum(stopwatch);
SumInt(stopwatch);
SumInt(stopwatch, param);
Sum(stopwatch);
DynamicSum(stopwatch);
SumInt(stopwatch);
SumInt(stopwatch, param);
Sum(stopwatch);
Console.ReadKey();
}
private static void Sum(Stopwatch stopwatch)
{
var sum = 0;
stopwatch.Reset();
stopwatch.Start();
for (int i = 0; i < ITERATIONS; i++)
{
sum += i;
}
stopwatch.Stop();
Console.WriteLine(string.Format("Elapsed {0}", stopwatch.ElapsedMilliseconds));
}
private static void SumInt(Stopwatch stopwatch)
{
var sum = new Sum();
stopwatch.Reset();
stopwatch.Start();
for (int i = 0; i < ITERATIONS; i++)
{
sum.intSum += i;
}
stopwatch.Stop();
Console.WriteLine(string.Format("Class Sum int Elapsed {0}", stopwatch.ElapsedMilliseconds));
}
private static void SumInt(Stopwatch stopwatch, dynamic param)
{
var sum = new Sum2();
stopwatch.Reset();
stopwatch.Start();
for (int i = 0; i < ITERATIONS; i++)
{
sum.intSum += i;
}
stopwatch.Stop();
Console.WriteLine(string.Format("Class Sum int Elapsed {0} {1}", stopwatch.ElapsedMilliseconds, param.GetType()));
}
private static void DynamicSum(Stopwatch stopwatch)
{
var sum = new Sum();
stopwatch.Reset();
stopwatch.Start();
for (int i = 0; i < ITERATIONS; i++)
{
sum.DynSum += i;
}
stopwatch.Stop();
Console.WriteLine(String.Format("Dynamic Sum Elapsed {0}", stopwatch.ElapsedMilliseconds));
}
c#
performance
dynamic
Lukasz Madon
sumber
sumber
Jawaban:
Ini kesepakatannya.
Untuk setiap ekspresi dalam program Anda yang berjenis dinamis, kompiler memancarkan kode yang menghasilkan satu "objek situs panggilan dinamis" yang mewakili operasi. Jadi, misalnya, jika Anda memiliki:
maka kompiler akan menghasilkan kode yang secara moral seperti ini. (Kode aktualnya sedikit lebih kompleks; ini disederhanakan untuk keperluan presentasi.)
Lihat bagaimana ini bekerja sejauh ini? Kami menghasilkan situs panggilan sekali , tidak peduli berapa kali Anda menelepon M. Situs panggilan hidup selamanya setelah Anda menghasilkannya sekali. Situs panggilan adalah objek yang mewakili "akan ada panggilan dinamis ke Foo di sini".
OK, jadi sekarang Anda sudah memiliki situs panggilan, bagaimana cara kerjanya?
Situs panggilan adalah bagian dari Dynamic Language Runtime. DLR mengatakan, "hmm, seseorang berusaha melakukan doa dinamis dari metode foo pada objek di sini. Apakah saya tahu sesuatu tentang itu? Tidak. Maka saya lebih baik mencari tahu."
DLR kemudian menginterogasi objek dalam d1 untuk melihat apakah itu sesuatu yang istimewa. Mungkin itu adalah objek COM warisan, atau objek Iron Python, atau objek Iron Ruby, atau objek IE DOM. Jika bukan salah satu dari itu maka itu harus menjadi objek C # biasa.
Ini adalah titik di mana kompiler mulai lagi. Tidak perlu lexer atau parser, jadi DLR memulai versi khusus dari kompiler C # yang hanya memiliki penganalisis metadata, penganalisis semantik untuk ekspresi, dan emitor yang memancarkan Pohon Ekspresi alih-alih IL.
Penganalisis metadata menggunakan Refleksi untuk menentukan jenis objek di d1, dan kemudian meneruskannya ke penganalisis semantik untuk menanyakan apa yang terjadi ketika objek tersebut dipanggil pada metode Foo. Penganalisa resolusi kelebihan beban angka keluar, dan kemudian membangun Pohon Ekspresi - sama seperti jika Anda memanggil Foo dalam pohon ekspresi lambda - yang mewakili panggilan itu.
Compiler C # kemudian meneruskan pohon ekspresi itu kembali ke DLR bersama dengan kebijakan cache. Kebijakan biasanya "kedua kalinya Anda melihat objek jenis ini, Anda dapat menggunakan kembali pohon ekspresi ini daripada memanggil saya kembali lagi". DLR kemudian memanggil Kompilasi pada pohon ekspresi, yang memanggil kompilator ekspresi-pohon-ke-IL dan mengeluarkan blok IL yang dihasilkan secara dinamis dalam delegasi.
DLR kemudian melakukan cache delegasi ini dalam cache yang terkait dengan objek situs panggilan.
Kemudian memanggil delegasi, dan panggilan Foo terjadi.
Kali kedua Anda menelepon M, kami sudah memiliki situs panggilan. DLR menginterogasi objek lagi, dan jika objek adalah jenis yang sama seperti terakhir kali, itu mengambil delegasi dari cache dan memanggilnya. Jika objek dari tipe yang berbeda maka cache meleset, dan seluruh proses dimulai lagi; kami melakukan analisis semantik panggilan dan menyimpan hasilnya dalam cache.
Ini terjadi untuk setiap ekspresi yang melibatkan dinamis. Jadi misalnya jika Anda memiliki:
lalu ada tiga situs panggilan dinamis. Satu untuk panggilan dinamis ke Foo, satu untuk penambahan dinamis, dan satu untuk konversi dinamis dari dinamis ke int. Masing-masing memiliki analisis runtime sendiri dan cache sendiri dari hasil analisis.
Masuk akal?
sumber
Pembaruan: Menambahkan tolok ukur yang dikompilasi sebelumnya dan yang dikompilasi malas
Pembaruan 2: Ternyata, saya salah. Lihat posting Eric Lippert untuk jawaban yang lengkap dan benar. Saya meninggalkan ini di sini demi nomor benchmark
* Pembaruan 3: Menambahkan tolok ukur IL-Emitted dan Lazy IL-Emitted, berdasarkan jawaban Mark Gravell untuk pertanyaan ini .
Sepengetahuan saya, penggunaandynamic
kata kunci tidak menyebabkan kompilasi tambahan saat runtime di dalam dan dari dirinya sendiri (meskipun saya membayangkan itu bisa melakukannya dalam keadaan tertentu, tergantung pada jenis objek apa yang mendukung variabel dinamis Anda).Mengenai kinerja,
dynamic
memang secara inheren memperkenalkan beberapa overhead, tetapi tidak sebanyak yang Anda pikirkan. Misalnya, saya baru saja menjalankan patokan yang terlihat seperti ini:Seperti yang dapat Anda lihat dari kode, saya mencoba memanggil metode no-op sederhana dengan tujuh cara berbeda:
dynamic
Action
yang dikompilasi pada saat runtime (sehingga tidak termasuk waktu kompilasi dari hasil).Action
yang dikompilasi pertama kali dibutuhkan, menggunakan variabel Malas non-thread-safe (dengan demikian termasuk waktu kompilasi)Masing-masing dipanggil 1 juta kali dalam satu loop sederhana. Berikut adalah hasil waktunya:
Jadi, sementara menggunakan
dynamic
kata kunci membutuhkan urutan lebih lama daripada memanggil metode secara langsung, masih berhasil menyelesaikan operasi sejuta kali dalam sekitar 50 milidetik, membuatnya jauh lebih cepat daripada refleksi. Jika metode yang kami panggil sedang mencoba melakukan sesuatu yang intensif, seperti menggabungkan beberapa string bersama-sama atau mencari koleksi untuk suatu nilai, operasi-operasi itu mungkin akan jauh lebih besar daripada perbedaan antara panggilan langsung dandynamic
panggilan.Kinerja hanyalah salah satu dari banyak alasan bagus untuk tidak menggunakan yang
dynamic
tidak perlu, tetapi ketika Anda berurusan dengandynamic
data yang sebenarnya , itu dapat memberikan keuntungan yang jauh lebih besar daripada kerugiannya.Perbarui 4
Berdasarkan komentar Johnbot, saya membagi area Refleksi menjadi empat tes terpisah:
... dan berikut adalah hasil benchmark:
Jadi, jika Anda dapat menentukan sebelumnya metode tertentu yang harus Anda panggil banyak, memohon delegasi yang di-cache merujuk pada metode itu adalah secepat memanggil metode itu sendiri. Namun, jika Anda perlu menentukan metode mana yang harus dihubungi saat Anda akan memintanya, membuat delegasi untuk itu sangat mahal.
sumber
dynamic
tentu saja kalah:public class ONE<T>{public object i { get; set; }public ONE(){i = typeof(T).ToString();}public object make(int ix){ if (ix == 0) return i;ONE<ONE<T>> x = new ONE<ONE<T>>();/*dynamic x = new ONE<ONE<T>>();*/return x.make(ix - 1);}}ONE<END> x = new ONE<END>();string lucky;Stopwatch sw = new Stopwatch();sw.Start();lucky = (string)x.make(500);sw.Stop();Trace.WriteLine(sw.ElapsedMilliseconds);Trace.WriteLine(lucky);
var methodDelegate = (Action)method.CreateDelegate(typeof(Action), foo);