Mam pytanie dotyczące wydajności dynamic
w języku C #. Czytałem, dynamic
że kompilator działa ponownie, ale co robi?
Czy musi przekompilować całą metodę ze dynamic
zmienną używaną jako parametr, czy tylko te wiersze z dynamicznym zachowaniem / kontekstem?
Zauważyłem, że użycie dynamic
zmiennych może spowolnić prostą pętlę for o 2 rzędy wielkości.
Kod, którym się bawił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));
}
c#
performance
dynamic
Łukasz Madon
źródło
źródło
Odpowiedzi:
Oto oferta.
Dla każdego wyrażenia w programie, które jest typu dynamicznego, kompilator emituje kod, który generuje pojedynczy „obiekt witryny wywołania dynamicznego”, który reprezentuje operację. Na przykład, jeśli masz:
wtedy kompilator wygeneruje kod moralnie podobny. (Rzeczywisty kod jest nieco bardziej złożony; jest to uproszczone do celów prezentacji).
Widzisz, jak to działa do tej pory? Generujemy witrynę wywoławczą raz , bez względu na to, ile razy dzwonisz do M. Witryna wywoławcza działa wiecznie po tym, jak ją raz wygenerujesz. Miejsce wywołania to obiekt, który reprezentuje „tutaj będzie dynamiczne wywołanie Foo”.
OK, więc teraz, gdy masz witrynę wywołań, jak działa wywołanie?
Witryna telefoniczna jest częścią środowiska wykonawczego języka dynamicznego. 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ę dowiedzę”.
DLR następnie przesłuchuje obiekt w d1, aby sprawdzić, czy jest coś specjalnego. Może jest to starszy obiekt COM, obiekt Iron Python, obiekt Iron Ruby lub obiekt IE DOM. Jeśli nie jest żadnym z nich, to musi to być zwykły obiekt C #.
To jest punkt, w którym kompilator uruchamia się ponownie. Nie ma potrzeby stosowania leksera 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 Reflection do określenia typu obiektu w d1, a następnie przekazuje go do analizatora semantycznego, aby zapytać, co się dzieje, gdy taki obiekt jest wywoływany metodą Foo. Analizator rozpoznawania przeciążenia wykrywa to, a następnie tworzy drzewo wyrażeń - tak jak w przypadku wywołania Foo w wyrażeniu lambda - które reprezentuje to wywołanie.
Kompilator C # następnie przekazuje to drzewo wyrażeń z powrotem do DLR wraz z zasadami pamięci podręcznej. Zasadą jest zazwyczaj, że „gdy drugi raz zobaczysz obiekt tego typu, możesz ponownie użyć tego drzewa wyrażeń, zamiast oddzwaniać do mnie ponownie”. Następnie DLR wywołuje Compile w drzewie wyrażeń, które wywołuje kompilator drzewa wyrażeń do IL i wypluwa blok dynamicznie generowanego IL w delegacie.
Następnie DLR buforuje tego delegata w pamięci podręcznej skojarzonej z obiektem wywołania lokacji.
Następnie wywołuje delegata i następuje wywołanie Foo.
Gdy drugi raz dzwonisz do M, mamy już witrynę internetową. DLR ponownie przesłuchuje obiekt, a jeśli obiekt jest tego samego typu, co ostatnim razem, pobiera delegata z pamięci podręcznej i wywołuje go. Jeśli obiekt jest innego typu, to pamięć podręczna nie działa i cały proces zaczyna się od nowa; wykonujemy analizę semantyczną połączenia i przechowujemy wynik w pamięci podręcznej.
Dzieje się tak w przypadku każdego wyrażenia zawierającego dynamikę. Na przykład, jeśli masz:
wtedy są trzy witryny dynamicznych połączeń. Jeden do dynamicznego wywołania Foo, jeden do dynamicznego dodawania i jeden do dynamicznej konwersji z dynamicznej na int. Każdy z nich ma własną analizę w czasie wykonywania i własną pamięć podręczną wyników analizy.
Ma sens?
źródło
Aktualizacja: Dodano wstępnie skompilowane i leniwie skompilowane testy porównawcze
Aktualizacja 2: Okazuje się, że się mylę. Pełna i poprawna odpowiedź znajduje się w poście Erica Lipperta. Zostawiam to tutaj ze względu na wartości wzorcowe
* Aktualizacja 3: Dodano testy porównawcze Emitowane przez IL i Leniwe emisje IL, w oparciu o odpowiedź Marka Gravella na to pytanie .
O ile mi wiadomo, użyciedynamic
słowa kluczowego samo w sobie nie powoduje żadnej dodatkowej kompilacji w czasie wykonywania (chociaż wyobrażam sobie, że mogłoby to zrobić w określonych okolicznościach, w zależności od typu obiektów, które stanowią kopię zapasową zmiennych dynamicznych).Jeśli chodzi o wydajność, z
dynamic
natury wprowadza pewne narzuty, ale nie tak bardzo, jak mogłoby się wydawać. Na przykład właśnie przeprowadziłem test porównawczy, który wygląda następująco:Jak widać z kodu, próbuję wywołać prostą metodę no-op na siedem różnych sposobów:
dynamic
Action
który został wstępnie skompilowany w czasie wykonywania (w ten sposób wykluczając czas kompilacji z wyników).Action
zmiennej, która jest kompilowana za pierwszym razem, gdy jest potrzebna, przy użyciu zmiennej Lazy, która nie jest bezpieczna dla wątków (w ten sposób obejmuje czas kompilacji)Każdy jest wywoływany milion razy w prostej pętli. Oto wyniki pomiaru czasu:
Tak więc, chociaż użycie
dynamic
słowa kluczowego trwa o rząd wielkości dłużej niż bezpośrednie wywołanie metody, nadal udaje mu się zakończyć operację milion razy w około 50 milisekund, co czyni ją znacznie szybszą niż odbicie. Gdyby wywoływana przez nas metoda próbowała wykonać coś intensywnego, na przykład połączyć kilka ciągów razem lub wyszukać w kolekcji wartość, operacje te prawdopodobnie znacznie przeważyłyby nad różnicą między wywołaniem bezpośrednim adynamic
wywołaniem.Wydajność to tylko jeden z wielu dobrych powodów, dla których nie należy używać ich
dynamic
niepotrzebnie, ale gdy masz do czynienia z prawdziwymidynamic
danymi, może zapewnić korzyści znacznie przewyższające wady.Zaktualizuj 4
Opierając się na komentarzu Johnbota, podzieliłem obszar refleksji na cztery oddzielne testy:
... a oto wyniki testów porównawczych:
Jeśli więc możesz z góry określić konkretną metodę, którą będziesz musiał często wywoływać, wywołanie delegata z pamięci podręcznej odnoszącego się do tej metody jest prawie tak szybkie, jak wywołanie samej metody. Jeśli jednak chcesz określić, którą metodę wywołać w momencie, gdy masz zamiar ją wywołać, utworzenie delegata jest bardzo kosztowne.
źródło
dynamic
oczywiście przegrywając: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);