Bagaimana pengaruh variabel dinamis terhadap kinerja?


128

Saya punya pertanyaan tentang kinerja dynamicdalam C #. Saya sudah baca dynamicmembuat kompiler berjalan lagi, tetapi apa fungsinya?

Apakah harus mengkompilasi ulang seluruh metode dengan dynamicvariabel yang digunakan sebagai parameter atau hanya garis-garis dengan perilaku / konteks dinamis?

Saya perhatikan bahwa menggunakan dynamicvariabel 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));
    }

Tidak, itu tidak menjalankan kompiler, yang akan membuatnya menghukum lambat pada pass pertama. Agak mirip dengan Refleksi tetapi dengan banyak kecerdasan untuk melacak apa yang dilakukan sebelumnya untuk meminimalkan overhead. Google "runtime bahasa dinamis" untuk wawasan lebih lanjut. Dan tidak, itu tidak akan pernah mendekati kecepatan loop 'asli'.
Hans Passant

Jawaban:


234

Saya telah membaca dinamis membuat kompiler berjalan lagi, tetapi apa fungsinya. Apakah harus mengkompilasi ulang seluruh metode dengan dinamis yang digunakan sebagai parameter atau lebih tepatnya garis-garis dengan perilaku / konteks dinamis (?)

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:

class C
{
    void M()
    {
        dynamic d1 = whatever;
        dynamic d2 = d1.Foo();

maka kompiler akan menghasilkan kode yang secara moral seperti ini. (Kode aktualnya sedikit lebih kompleks; ini disederhanakan untuk keperluan presentasi.)

class C
{
    static DynamicCallSite FooCallSite;
    void M()
    {
        object d1 = whatever;
        object d2;
        if (FooCallSite == null) FooCallSite = new DynamicCallSite();
        d2 = FooCallSite.DoInvocation("Foo", d1);

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:

int x = d1.Foo() + d2;

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?


Hanya karena penasaran, versi kompiler khusus tanpa parser / lexer dipanggil dengan memberikan flag khusus ke csc.exe standar?
Roman Royter

@ Eric, dapatkah saya menyulitkan Anda untuk mengarahkan saya ke posting blog Anda sebelumnya di mana Anda berbicara tentang konversi implisit singkat, int, dll? Seingat saya Anda sebutkan di sana bagaimana / mengapa menggunakan dinamis dengan Convert.ToXXX menyebabkan kompiler menyala. Saya yakin saya membantai detailnya, tapi mudah-mudahan Anda tahu apa yang saya bicarakan.
Adam Rackis

4
@ Roman: Tidak. Csc.exe ditulis dalam C ++, dan kami membutuhkan sesuatu yang dapat dengan mudah kami hubungi dari C #. Juga, kompiler arus utama memiliki objek tipe sendiri, tetapi kami harus dapat menggunakan objek tipe Refleksi. Kami mengekstraksi bagian-bagian yang relevan dari kode C ++ dari kompiler csc.exe dan menerjemahkannya baris demi baris ke dalam C #, dan kemudian membangun sebuah pustaka dari itu untuk panggilan DLR.
Eric Lippert

9
@Eric, "Kami mengekstraksi bagian-bagian yang relevan dari kode C ++ dari kompiler csc.exe dan menerjemahkannya baris demi baris ke dalam C #" apakah itu tentang orang-orang yang mengira Roslyn layak untuk mengejar :)
ShuggyCoUk

5
@ShuggyCoUk: Gagasan memiliki compiler-as-a-service telah muncul selama beberapa waktu, tetapi sebenarnya membutuhkan layanan runtime untuk melakukan analisis kode adalah dorongan besar terhadap proyek itu, ya.
Eric Lippert

108

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, penggunaan dynamickata 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, dynamicmemang secara inheren memperkenalkan beberapa overhead, tetapi tidak sebanyak yang Anda pikirkan. Misalnya, saya baru saja menjalankan patokan yang terlihat seperti ini:

void Main()
{
    Foo foo = new Foo();
    var args = new object[0];
    var method = typeof(Foo).GetMethod("DoSomething");
    dynamic dfoo = foo;
    var precompiled = 
        Expression.Lambda<Action>(
            Expression.Call(Expression.Constant(foo), method))
        .Compile();
    var lazyCompiled = new Lazy<Action>(() =>
        Expression.Lambda<Action>(
            Expression.Call(Expression.Constant(foo), method))
        .Compile(), false);
    var wrapped = Wrap(method);
    var lazyWrapped = new Lazy<Func<object, object[], object>>(() => Wrap(method), false);
    var actions = new[]
    {
        new TimedAction("Direct", () => 
        {
            foo.DoSomething();
        }),
        new TimedAction("Dynamic", () => 
        {
            dfoo.DoSomething();
        }),
        new TimedAction("Reflection", () => 
        {
            method.Invoke(foo, args);
        }),
        new TimedAction("Precompiled", () => 
        {
            precompiled();
        }),
        new TimedAction("LazyCompiled", () => 
        {
            lazyCompiled.Value();
        }),
        new TimedAction("ILEmitted", () => 
        {
            wrapped(foo, null);
        }),
        new TimedAction("LazyILEmitted", () => 
        {
            lazyWrapped.Value(foo, null);
        }),
    };
    TimeActions(1000000, actions);
}

class Foo{
    public void DoSomething(){}
}

static Func<object, object[], object> Wrap(MethodInfo method)
{
    var dm = new DynamicMethod(method.Name, typeof(object), new Type[] {
        typeof(object), typeof(object[])
    }, method.DeclaringType, true);
    var il = dm.GetILGenerator();

    if (!method.IsStatic)
    {
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Unbox_Any, method.DeclaringType);
    }
    var parameters = method.GetParameters();
    for (int i = 0; i < parameters.Length; i++)
    {
        il.Emit(OpCodes.Ldarg_1);
        il.Emit(OpCodes.Ldc_I4, i);
        il.Emit(OpCodes.Ldelem_Ref);
        il.Emit(OpCodes.Unbox_Any, parameters[i].ParameterType);
    }
    il.EmitCall(method.IsStatic || method.DeclaringType.IsValueType ?
        OpCodes.Call : OpCodes.Callvirt, method, null);
    if (method.ReturnType == null || method.ReturnType == typeof(void))
    {
        il.Emit(OpCodes.Ldnull);
    }
    else if (method.ReturnType.IsValueType)
    {
        il.Emit(OpCodes.Box, method.ReturnType);
    }
    il.Emit(OpCodes.Ret);
    return (Func<object, object[], object>)dm.CreateDelegate(typeof(Func<object, object[], object>));
}

Seperti yang dapat Anda lihat dari kode, saya mencoba memanggil metode no-op sederhana dengan tujuh cara berbeda:

  1. Panggilan metode langsung
  2. Menggunakan dynamic
  3. Dengan refleksi
  4. Menggunakan Actionyang dikompilasi pada saat runtime (sehingga tidak termasuk waktu kompilasi dari hasil).
  5. Menggunakan sebuah Actionyang dikompilasi pertama kali dibutuhkan, menggunakan variabel Malas non-thread-safe (dengan demikian termasuk waktu kompilasi)
  6. Menggunakan metode yang dihasilkan secara dinamis yang akan dibuat sebelum tes.
  7. Menggunakan metode yang dihasilkan secara dinamis yang menjadi malas dipakai selama tes.

Masing-masing dipanggil 1 juta kali dalam satu loop sederhana. Berikut adalah hasil waktunya:

Langsung: 3,4248ms
Dinamis: 45,0728 ms
Refleksi: 888,4011ms
Prekompilasi: 21,9166ms
MalasDilengkapi: 30,2045ms
ILEmitted: 8,4918ms
MalasILEmitted: 14,3483ms

Jadi, sementara menggunakan dynamickata 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 dan dynamicpanggilan.

Kinerja hanyalah salah satu dari banyak alasan bagus untuk tidak menggunakan yang dynamictidak perlu, tetapi ketika Anda berurusan dengan dynamicdata 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:

    new TimedAction("Reflection, find method", () => 
    {
        typeof(Foo).GetMethod("DoSomething").Invoke(foo, args);
    }),
    new TimedAction("Reflection, predetermined method", () => 
    {
        method.Invoke(foo, args);
    }),
    new TimedAction("Reflection, create a delegate", () => 
    {
        ((Action)method.CreateDelegate(typeof(Action), foo)).Invoke();
    }),
    new TimedAction("Reflection, cached delegate", () => 
    {
        methodDelegate.Invoke();
    }),

... dan berikut adalah hasil benchmark:

masukkan deskripsi gambar di sini

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.


2
Respons yang terperinci, terima kasih! Saya bertanya-tanya tentang angka yang sebenarnya juga.
Sergey Sirotkin

4
Nah, kode dinamis memulai importir metadata, penganalisa semantik dan pohon emitor ekspresi dari kompiler, dan kemudian menjalankan kompiler ekspresi-tree-to-il pada output itu, jadi saya pikir itu adil untuk mengatakan bahwa itu dimulai naik kompiler saat runtime. Hanya karena tidak menjalankan lexer dan parser sepertinya tidak relevan.
Eric Lippert

6
Angka kinerja Anda tentu menunjukkan bagaimana kebijakan caching agresif DLR terbayar. Jika contoh Anda melakukan hal-hal konyol, seperti misalnya jika Anda memiliki tipe penerima yang berbeda setiap kali Anda melakukan panggilan, maka Anda akan melihat bahwa versi dinamis sangat lambat ketika tidak dapat memanfaatkan cache dari hasil analisis yang dikompilasi sebelumnya. . Tetapi ketika itu bisa mengambil keuntungan dari itu, kebaikan suci selalu cepat.
Eric Lippert

1
Sesuatu yang konyol sesuai saran Eric. Uji dengan menukar baris mana yang dikomentari. 8964ms vs 814ms, dengan dynamictentu 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);
Brian

1
var methodDelegate = (Action)method.CreateDelegate(typeof(Action), foo);
Adil
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.