Ponownie wrócę do tematyki kompilowania C# w locie. Pisałem wcześniej, że potrzebowałem takiej funkcji aby użytkownicy mojej aplikacji mogli w dowolnym momencie zdefiniować własny algorytm obliczania odległości między dwoma wektorami.
Po kilku próbach już wiedziałem jak to zrobić, a chwilę później miałem już zaimplementowaną pierwszą wersję rozwiązania. Przyszła pora wypróbowania nowej zabawki na prawdziwych danych. Uruchomiłem więc aplikację, napisałem krótki skrypt i wystartowałem obliczenia. W tym momencie aplikacja zamarła i było widać, że pożera wszelkie dostępne zasoby. Spodziewałem się czegoś innego :). Skończyło się zabiciem aplikacji. Co zrobiłem nie tak?
Dane jakich użyłem zawierały kilkaset tysięcy wektorów. Oznacza to, że odległość między wektorami musiała zostać obliczona "dużoooooooooooooooooooooo" razy. Każda taka operacja powodowała wywołanie omówionej już metody Evaluator.Run, czyli za każdym razem ten skrypt był ponownie kompilowany, a to trochę trwa. Ziarnko do ziarnka, a zbierze się miarka jak mówi przysłowie. Co więcej każde wywołanie Evaluator.Run powoduje załadowanie do pamięci kolejnego dynamicznie utworzonego modułu. Moduły te są potem usuwane ale to też trwa. W ramach eksperymentu proponuję uruchomić pod kontrolą debugger'a taki kod:
Zamiast zobaczyć, że kod wykonuje się błyskawicznie, zaobserwujemy jak VS próbuje załadować symbole dla kolejnych modułów (komunikat w rodzaju Loading symbols for eval-120... na pasku stanu). Jeśli postawimy pułapkę wewnątrz pętli i otworzymy okno Modules to na liście znajdziemy np.: eval-100, eval-101, eval-102...
W takiej sytuacji skrypt należy skompilować raz, a potem wielokrotnie używać wyniku kompilacji. Służy do tego metoday Evaluator.Compile, która zwraca instancję klasy CompiledMethod. Jeśli chcemy wywołać tak przygotowany skrypt korzystamy z metody CompiledMethod.Invoke. Nic trudnego. Parametry do takiego skompilowanego skryptu można przekazać w dokładnie taki sam sposób jak opisałem w poprzednim poście. Po zmianach powyższy kod będzie wyglądał jak poniżej i wykona się błyskawicznie:
Na koniec dodam, że po zastosowaniu takiego zabiegu nie widzę różnicy w czasie odpowiedzi pomiędzy przypadkiem kiedy używam algorytmu osadzonego/wkompilowanego w aplikację, a algorytmu dostarczonego przez użytkownika.
Po kilku próbach już wiedziałem jak to zrobić, a chwilę później miałem już zaimplementowaną pierwszą wersję rozwiązania. Przyszła pora wypróbowania nowej zabawki na prawdziwych danych. Uruchomiłem więc aplikację, napisałem krótki skrypt i wystartowałem obliczenia. W tym momencie aplikacja zamarła i było widać, że pożera wszelkie dostępne zasoby. Spodziewałem się czegoś innego :). Skończyło się zabiciem aplikacji. Co zrobiłem nie tak?
Dane jakich użyłem zawierały kilkaset tysięcy wektorów. Oznacza to, że odległość między wektorami musiała zostać obliczona "dużoooooooooooooooooooooo" razy. Każda taka operacja powodowała wywołanie omówionej już metody Evaluator.Run, czyli za każdym razem ten skrypt był ponownie kompilowany, a to trochę trwa. Ziarnko do ziarnka, a zbierze się miarka jak mówi przysłowie. Co więcej każde wywołanie Evaluator.Run powoduje załadowanie do pamięci kolejnego dynamicznie utworzonego modułu. Moduły te są potem usuwane ale to też trwa. W ramach eksperymentu proponuję uruchomić pod kontrolą debugger'a taki kod:
var settings = new CompilerSettings(); var ev = new Evaluator(new CompilerContext(settings, new ConsoleReportPrinter())); for (int i = 0; i < 1000; ++i) ev.Run("System.Console.WriteLine(\"Hello!\");");
Zamiast zobaczyć, że kod wykonuje się błyskawicznie, zaobserwujemy jak VS próbuje załadować symbole dla kolejnych modułów (komunikat w rodzaju Loading symbols for eval-120... na pasku stanu). Jeśli postawimy pułapkę wewnątrz pętli i otworzymy okno Modules to na liście znajdziemy np.: eval-100, eval-101, eval-102...
W takiej sytuacji skrypt należy skompilować raz, a potem wielokrotnie używać wyniku kompilacji. Służy do tego metoday Evaluator.Compile, która zwraca instancję klasy CompiledMethod. Jeśli chcemy wywołać tak przygotowany skrypt korzystamy z metody CompiledMethod.Invoke. Nic trudnego. Parametry do takiego skompilowanego skryptu można przekazać w dokładnie taki sam sposób jak opisałem w poprzednim poście. Po zmianach powyższy kod będzie wyglądał jak poniżej i wykona się błyskawicznie:
var settings = new CompilerSettings(); var ev = new Evaluator(new CompilerContext(settings, new ConsoleReportPrinter())); var method = ev.Compile("System.Console.WriteLine(\"Hello!\");"); object o = null; for (int i = 0; i < 1000; ++i) method.Invoke(ref o);
Na koniec dodam, że po zastosowaniu takiego zabiegu nie widzę różnicy w czasie odpowiedzi pomiędzy przypadkiem kiedy używam algorytmu osadzonego/wkompilowanego w aplikację, a algorytmu dostarczonego przez użytkownika.