Dinamik bir değişkene sahip olmak performansı nasıl etkiler?


128

İn dynamicC # performansı hakkında bir sorum var . Okudum dynamicderleyiciyi tekrar çalıştırır, ama ne işe yarar?

dynamicBir parametre olarak kullanılan değişkenle veya sadece dinamik davranış / bağlam içeren satırlarla tüm yöntemi yeniden derlemek zorunda mı ?

dynamicDeğişkenleri kullanmanın basit bir for döngüsünü 2 kat yavaşlattığını fark ettim .

Oynadığım kod:

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));
    }

Hayır, derleyiciyi çalıştırmaz, bu onu ilk geçişte yavaşlatır. Reflection'a biraz benzer, ancak ek yükü en aza indirmek için daha önce ne yapıldığını takip etmek için çok sayıda akıllıca var. Daha fazla bilgi için Google "dinamik dil çalışma zamanı". Ve hayır, asla 'yerel' bir döngünün hızına yaklaşmayacak.
Hans Passant

Yanıtlar:


235

Dinamik okudum, derleyiciyi tekrar çalıştırır, ama ne yapar. Bir parametre olarak kullanılan dinamik ile tüm yöntemi yeniden derlemek zorunda mı, yoksa dinamik davranış / bağlam içeren satırlar (?)

İşte anlaşma.

Programınızdaki dinamik türdeki her ifade için, derleyici işlemi temsil eden tek bir "dinamik çağrı sitesi nesnesi" oluşturan kod yayar. Öyleyse, örneğin, varsa:

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

daha sonra derleyici ahlaki olarak buna benzer bir kod üretecektir. (Gerçek kod biraz daha karmaşıktır; bu, sunum amacıyla basitleştirilmiştir.)

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

Şimdiye kadar bunun nasıl çalıştığını görüyor musunuz? M'yi kaç kez ararsanız arayın, çağrı sitesini bir kez oluştururuz. Çağrı sitesi , siz onu bir kez oluşturduktan sonra sonsuza kadar yaşar. Çağrı sitesi, "burada Foo'ya dinamik bir çağrı olacak" ifadesini temsil eden bir nesnedir.

Tamam, şimdi arama sitesine sahip olduğunuza göre, çağrı nasıl işliyor?

Çağrı sitesi, Dinamik Dil Çalışma Zamanı'nın bir parçasıdır. DLR "hmm, birisi bu nesnede foo yönteminin dinamik bir çağrısını yapmaya çalışıyor. Bununla ilgili bir şey biliyor muyum? Hayır. O halde bulsam iyi olur."

DLR daha sonra özel bir şey olup olmadığını görmek için d1'deki nesneyi sorgular. Belki eski bir COM nesnesi veya bir Iron Python nesnesi veya bir Iron Ruby nesnesi veya bir IE DOM nesnesidir. Bunlardan herhangi biri değilse, o zaman sıradan bir C # nesnesi olmalıdır.

Derleyicinin yeniden başladığı nokta budur. Bir sözcükçü veya ayrıştırıcıya gerek yoktur, bu nedenle DLR, yalnızca meta veri çözümleyicisine, ifadeler için anlamsal çözümleyiciye ve IL yerine İfade Ağaçları yayan bir yayıcıya sahip olan C # derleyicisinin özel bir sürümünü başlatır.

Meta veri çözümleyicisi, d1'deki nesnenin türünü belirlemek için Yansıtma'yı kullanır ve daha sonra, Foo yönteminde böyle bir nesne çağrıldığında ne olacağını sormak için bunu anlamsal çözümleyiciye iletir. Aşırı yük çözümleme çözümleyicisi bunu çözer ve sonra bir İfade Ağacı oluşturur - tıpkı lambda ifade ağacında Foo olarak adlandırmışsınız gibi - bu çağrıyı temsil eder.

C # derleyicisi daha sonra bu ifade ağacını önbellek ilkesiyle birlikte DLR'ye geri gönderir. İlke genellikle "bu türden bir nesneyi ikinci kez gördüğünüzde, beni tekrar aramak yerine bu ifade ağacını yeniden kullanabilirsiniz" şeklindedir. DLR daha sonra ifade ağacında Compile'ı çağırır, bu da ifade ağacından IL'ye derleyiciyi çağırır ve bir temsilcide dinamik olarak oluşturulmuş IL bloğunu dağıtır.

DLR daha sonra bu delegeyi arama sitesi nesnesiyle ilişkili bir önbellekte önbelleğe alır.

Ardından temsilciyi çağırır ve Foo çağrısı gerçekleşir.

M'yi ikinci aradığınızda, zaten bir çağrı sitemiz var. DLR, nesneyi tekrar sorgular ve nesne geçen seferki ile aynı türdeyse, temsilciyi önbellekten alır ve onu çağırır. Nesne farklı bir türdeyse, önbellek kaybolur ve tüm süreç yeniden başlar; çağrının anlamsal analizini yapar ve sonucu önbellekte depolar.

Bu , dinamik içeren her ifade için olur . Örneğin, sahipseniz:

int x = d1.Foo() + d2;

Sonra orada üç dinamik aramalar siteleri. Biri Foo'ya dinamik çağrı, biri dinamik toplama ve diğeri dinamikten int'e dinamik dönüşüm için. Her birinin kendi çalışma zamanı analizi ve kendi analiz sonuçları önbelleği vardır.

Mantıklı olmak?


Merak ettiğim için, ayrıştırıcı / lexer içermeyen özel derleyici sürümü, standart csc.exe'ye özel bir bayrak iletilerek çalıştırılır.
Roman Royter

@Eric, kısa, int vb. Gibi örtük dönüşümler hakkında konuştuğunuz önceki bir blog gönderinize beni yönlendirmenizi rica edebilir miyim? Orada bahsettiğinizi hatırladığım gibi, Convert.ToXXX ile dinamik kullanımının nasıl / neden derleyicinin çalışmasına neden olduğu. Eminim ayrıntıları parçalıyorum, ama umarım neden bahsettiğimi biliyorsunuzdur.
Adam Rackis

4
@Roman: Hayır. Csc.exe C ++ ile yazılmış ve C # 'dan kolayca arayabileceğimiz bir şeye ihtiyacımız vardı. Ayrıca, ana hat derleyicisinin kendi yazı nesneleri vardır, ancak Yansıma türü nesneleri kullanabilmemiz gerekiyordu. C ++ kodunun ilgili kısımlarını csc.exe derleyicisinden çıkardık ve satır satır C # 'a çevirdik ve ardından DLR'nin çağırması için bundan bir kitaplık oluşturduk.
Eric Lippert

9
@Eric, bu konuda o zaman insanlar :) Roslyn değerinde bunların alçaklık olabileceğini düşündüm oldu "We C csc.exe derleyici ++ kod ilgili bölümlerini çıkarılan ve C # içine hat-by-line bunları tercüme"
ShuggyCoUk

5
@ShuggyCoUk: Hizmet olarak bir derleyiciye sahip olma fikri bir süredir ortalıkta dolaşıyordu, ancak aslında kod analizi için bir çalışma zamanı hizmetine ihtiyaç duymak bu projeye doğru büyük bir ivme kazandırdı, evet.
Eric Lippert

108

Güncelleme: Önceden derlenmiş ve tembel derlenmiş karşılaştırmalar eklendi

Güncelleme 2: Anlaşıldı, yanılıyorum. Tam ve doğru bir cevap için Eric Lippert'in gönderisine bakın. Bunu kıyaslama rakamları uğruna burada bırakıyorum

* Güncelleme 3: Mark Gravell'in bu soruya verdiği cevaba göre IL-Emitted ve Lazy IL-Emitted benchmarkları eklendi .

Bildiğim dynamickadarıyla , anahtar kelimenin kullanımı kendi başına çalışma zamanında fazladan bir derlemeye neden olmaz (dinamik değişkenlerinizi hangi tür nesnelerin desteklediğine bağlı olarak belirli koşullar altında yapabileceğini düşünmeme rağmen).

Performansla ilgili olarak dynamic, doğal olarak bir miktar ek yük getirir, ancak düşündüğünüz kadar değil. Örneğin, şuna benzeyen bir kıyaslama yaptım:

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>));
}

Koddan da görebileceğiniz gibi, yedi farklı yoldan basit bir işlemsiz yöntemi çağırmaya çalışıyorum:

  1. Doğrudan yöntem çağrısı
  2. kullanma dynamic
  3. Yansıma yoluyla
  4. Çalışma Actionzamanında önceden derlenmiş bir kullanım (bu nedenle sonuçlardan derleme süresi hariç).
  5. Bir kullanma Action(böylece derleme zamanında dahil) olmayan bir iş parçacığı güvenli Tembel değişkenini kullanarak, ihtiyaç duyulan ilk kez derlenmiş olduğu
  6. Testten önce oluşturulan dinamik olarak oluşturulmuş bir yöntemi kullanma.
  7. Test sırasında tembel olarak örneklenen dinamik olarak oluşturulmuş bir yöntem kullanma.

Her biri basit bir döngüde 1 milyon kez çağrılır. İşte zamanlama sonuçları:

Doğrudan: 3.4248ms
Dinamik: 45.0728ms
Yansıma: 888.4011ms
Önceden
Derlenmiş: 21.9166ms
Tembel Derlenmiş: 30.2045ms ILEanned: 8.4918ms
Tembelleştirilmiş: 14.3483ms

Bu nedenle, dynamicanahtar kelimeyi kullanmak, yöntemi doğrudan çağırmaktan daha uzun bir sıra alırken, işlemi yaklaşık 50 milisaniyede bir milyon kez tamamlamayı başarır ve bu da onu yansımadan çok daha hızlı hale getirir. Dediğimiz yöntem, birkaç dizeyi bir araya getirmek veya bir koleksiyonda bir değer aramak gibi yoğun bir şey yapmaya çalışıyor olsaydı, bu işlemler büyük olasılıkla doğrudan arama ile arama arasındaki farktan çok daha ağır basacaktır dynamic.

Performans, dynamicgereksiz yere kullanmamak için birçok iyi nedenden sadece biridir , ancak gerçek dynamicverilerle uğraşırken , dezavantajlardan çok daha ağır basan avantajlar sağlayabilir.

Güncelleme 4

Johnbot'un yorumuna dayanarak, Yansıma alanını dört ayrı teste böldüm:

    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();
    }),

... ve işte karşılaştırma sonuçları:

görüntü açıklamasını buraya girin

Bu nedenle, çok fazla aramanız gereken belirli bir yöntemi önceden belirleyebilirseniz, bu yönteme atıfta bulunan önbelleğe alınmış bir temsilciyi çağırmak, yöntemin kendisini çağırmak kadar hızlıdır. Ancak, tam da onu çağırmak üzereyken hangi yöntemi arayacağınızı belirlemeniz gerekiyorsa, bunun için bir temsilci oluşturmak çok pahalıdır.


2
Böyle ayrıntılı bir yanıt, teşekkürler! Ben de gerçek rakamları merak ediyordum.
Sergey Sirotkin

4
Dinamik kod, derleyicinin meta veri içe aktarıcısını, anlamsal çözümleyiciyi ve ifade ağacı yayıcısını başlatır ve ardından bunun çıktısı üzerinde bir ifade ağacından il'e derleyici çalıştırır, bu yüzden başladığını söylemenin adil olduğunu düşünüyorum. derleyiciyi çalışma zamanında yükseltin. Lexer'ı çalıştırmadığı ve ayrıştırıcı pek alakalı görünmediği için.
Eric Lippert

6
Performans rakamlarınız kesinlikle DLR'nin agresif önbelleğe alma politikasının nasıl sonuç verdiğini gösteriyor. Örneğiniz saçma şeyler yaptıysa, örneğin aramayı her yaptığınızda farklı bir alıcı türüne sahipseniz, dinamik sürümün önceden derlenmiş analiz sonuçlarının önbelleğinden yararlanamadığında çok yavaş olduğunu görürsünüz. . O zaman Ama olabilir bu avantajı, kutsal iyilik şimdiye hızlı kadar.
Eric Lippert

1
Eric'in önerisine göre saçma bir şey. Hangi satırın yorumlandığını değiştirerek test edin. 8964ms vs 814ms, dynamictabii ki kaybederek: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
Yöntem bilgilerinden bir temsilci oluşturmak ve düşünmek için adil olun:var methodDelegate = (Action)method.CreateDelegate(typeof(Action), foo);
Johnbot
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.