Jak zmienna dynamiczna wpływa na wydajność?

Mam pytanie odnośnie wydajności dynamiki w C#. Czytałem dynamic sprawia, że kompilator działa ponownie, ale co to robi?

Czy musi przekompilować całą metodę ze zmienną dynamiczną używaną jako parametr, czy tylko te linie z dynamicznym zachowaniem / kontekstem?

Zauważyłem, że używanie zmiennych dynamicznych może spowolnić prostą pętlę for o 2 rzędy wielkości.

Kod którym grałem:

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));
    }
Author: numaroth, 2011-09-20

2 answers

Czytałem dynamic sprawia, że kompilator działa ponownie, ale co robi. Czy musi przekompilować całą metodę z parametrem dynamic używanym jako parametr, czy raczej te linie z dynamicznym zachowaniem / kontekstem (?)

Umowa jest taka.

Dla każdego wyrażenia w programie typu dynamic kompilator emituje kod, który generuje pojedynczy "dynamiczny obiekt call site", który reprezentuje operację. Więc, na przykład, jeśli masz:

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

Wtedy kompilator wygeneruje kod, który jest moralnie taki jak ten. (Rzeczywisty kod jest nieco bardziej złożony; jest to uproszczone do celów prezentacji.)

class C
{
    static DynamicCallSite FooCallSite;
    void M()
    {
        object d1 = whatever;
        object d2;
        if (FooCallSite == null) FooCallSite = new DynamicCallSite();
        d2 = FooCallSite.DoInvocation("Foo", d1);
Widzisz jak to działa? Generujemy stronę wywołania raz , bez względu na to, ile razy dzwonisz do M. strona wywołania żyje wiecznie po wygenerowaniu jej raz. Strona wywołania jest obiektem, który reprezentuje "będzie tu dynamiczne połączenie do Foo".

OK, więc teraz, gdy masz stronę połączeń, jak to zrobić praca inwokacyjna?

Strona wywołania jest częścią środowiska Runtime Dynamic Language. DLR mówi: "hmm, ktoś próbuje wykonać dynamiczne wywołanie metody foo na tym obiekcie. Czy ja coś o tym wiem? Nie. Więc lepiej się dowiem."

DLR następnie przesłuchuje obiekt w d1, aby sprawdzić, czy jest to coś specjalnego. Być może jest to starszy obiekt COM, obiekt Iron Python, obiekt Iron Ruby lub obiekt IE DOM. Jeśli nie jest to żaden z nich to musi być zwykły C # object.

To jest punkt, w którym kompilator uruchamia się ponownie. Nie ma potrzeby stosowania lexera ani parsera, więc DLR uruchamia specjalną wersję kompilatora C#, która ma tylko analizator metadanych, analizator semantyczny dla wyrażeń i emiter, który emituje drzewa wyrażeń zamiast IL.

Analizator metadanych używa refleksji do określenia typu obiektu w d1, a następnie przekazuje go do analizatora semantycznego, aby zapytać, co się stanie, gdy taki obiekt zostanie wywołany na metodzie Foo. Analizator rozdzielczości przeciążeń wylicza to, a następnie buduje drzewo wyrażeń - tak, jakbyś nazwał Foo w drzewie wyrażeń lambda-które reprezentuje to wywołanie.

Kompilator C# przekazuje drzewo wyrażeń z powrotem do DLR wraz z zasadami pamięci podręcznej. Zasada zwykle brzmi: "gdy drugi raz zobaczysz obiekt tego typu, możesz ponownie użyć tego drzewa wyrażeń, zamiast oddzwaniać do mnie". DLR następnie wywołuje kompilację na drzewie wyrażeń, które wywołuje kompilator expression-tree-to-IL i wypluwa blok dynamicznie generowanego IL w delegacie.

DLR następnie buforuje tego delegata w pamięci podręcznej powiązanej z obiektem call site.

Następnie wywołuje delegata i następuje wywołanie Foo.

Kiedy drugi raz zadzwonisz do M, mamy już stronę połączeń. DLR ponownie przesłuchuje obiekt, a jeśli obiekt jest tego samego typu, co poprzednio, pobiera delegata z pamięci podręcznej i wywołuje go. Jeśli obiekt jest innego typu niż pamięć podręczna, a cały proces zaczyna się od nowa; wykonujemy analizę semantyczną wywołania i przechowujemy wynik w pamięci podręcznej.

Dzieje się tak dla każdego wyrażenia , które obejmuje dynamikę. Więc na przykład jeśli masz:

int x = d1.Foo() + d2;

Następnie są trzy dynamiczne strony połączeń. Jeden dla dynamicznego wywołania Foo, jeden dla dynamicznego dodawania i jeden dla dynamicznej konwersji z dynamicznego na int. Każdy z nich ma swój własny runtime analiza i własne cache wyników analizy.

Ma sens?

 190
Author: Eric Lippert,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-09-20 16:53:19

aktualizacja: Dodano prekompilowane i Lazy-skompilowane benchmarki

Update 2: okazuje się, że się mylę. Zobacz post Erica Lipperta, aby uzyskać pełną i poprawną odpowiedź. Zostawiam to tutaj ze względu na liczby odniesienia

*Update 3: dodano benchmarki IL-emitowane i Lazy IL-emitowane, oparte na odpowiedzi Marka Gravella na to pytanie .

z mojej wiedzy użycie słowa kluczowego dynamic nie powoduje żadnej dodatkowej kompilacji w czasie wykonywania w i samego siebie (chociaż wyobrażam sobie, że może to zrobić w konkretnych okolicznościach, w zależności od tego, jakiego typu obiekty wspierają twoje zmienne dynamiczne).

Jeśli chodzi o wydajność, dynamic z natury wprowadza pewne koszty, ale nie tak bardzo, jak mogłoby się wydawać. Na przykład po prostu uruchomiłem benchmark, który wygląda tak:

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

Jak widać z kodu, próbuję wywołać prostą metodę no-op na siedem różnych sposobów:

  1. metoda bezpośrednia call
  2. używając dynamic
  3. przez odbicie
  4. używając Action, który został wstępnie skompilowany w czasie wykonywania(wykluczając tym samym czas kompilacji z wyników).
  5. Nie jest to jednak możliwe, ponieważ nie jest to możliwe, ponieważ nie jest to możliwe, ponieważ nie jest to możliwe.]}
  6. przy użyciu dynamicznie generowanej metody, która zostanie utworzona przed testem.
  7. Za pomocą dynamicznie generowanej metody, która leniwie tworzy instancję podczas test.

Każdy zostanie wywołany milion razy w prostej pętli. Oto wyniki czasowe:

Direct: 3.4248 ms
Dynamiczny: 45.0728 ms
Reflection: 888.4011 ms
Precompiled: 21.9166 ms
LazyCompiled: 30.2045 ms
ILEmitted: 8.4918 ms
Lazyilemited: 14.3483 ms

Tak więc użycie słowa kluczowego dynamic trwa o rząd wielkości dłużej niż bezpośrednie wywołanie metody, nadal udaje mu się zakończyć operację a milion razy w około 50 milisekund, co czyni go znacznie szybszym niż odbicie. Jeśli wywołana przez nas metoda próbuje zrobić coś intensywnego, jak połączenie kilku łańcuchów lub przeszukiwanie kolekcji dla wartości, operacje te prawdopodobnie znacznie przewyższają różnicę między wywołaniem bezpośrednim a wywołaniem dynamic.

Wydajność jest tylko jednym z wielu dobrych powodów, aby niepotrzebnie nie używać dynamic, ale kiedy masz do czynienia z prawdziwymi dynamic danymi, może to zapewnić korzyści, które znacznie przewyższają wady.

Update 4

Bazując na komentarzu Johnbota, podzieliłem obszar refleksji na cztery oddzielne testy:

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

... a oto wyniki benchmarka:

Tutaj wpisz opis obrazka

Więc jeśli możesz z góry określić konkretną metodę, którą będziesz musiał często wywoływać, wywołanie buforowanego delegata odnoszącego się do tej metody jest mniej więcej tak szybkie, jak wywołanie samej metody. Jeśli jednak musisz określić, którą metodę wywołać, gdy masz zamiar wywołaj go, tworzenie delegata dla niego jest bardzo kosztowne.

 82
Author: StriplingWarrior,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 10:31:09