22/03/2013

Konwencja wołania

Home

Konwencja wołania (ang. Calling convention) to zestaw zasad, który określa w jaki sposób metoda wołająca przekazuje parametry do metody wołanej i odbiera od niej wyniki. Programując w .NET, o ile nie współpracujemy z kodem natywnym, w ogóle nie musimy się tym interesować. Zgłębiając ten temat można jednak nauczyć się kilku ciekawych rzeczy.

Zacznijmy od tego jakiej konwencji wołania używa CLR. Na to pytanie można odpowiedzieć na dwa sposoby. Odpowiedź "wysoko poziomowa" brzmi: CLR został zaimplemntowany jako maszyna stosowa i nie używa rejestrów, a więc parametry oraz wyniki zostaną przekazane na stosie. Oto prosty przykład, zacznijmy od klasy:

public class Test
{
        public string ReturnString(int i, bool b)
        {
            return i.ToString() + b.ToString();
        }

        public string ReturnString(Fun f)
        {
            return f.ToString();
        }
}

Teraz spójrzmy na kod w C# i odpowiadający mu kod IL, w którym użyto tych metod:

var t = new Test();

newobj instance void ConsoleApplication1.Test::.ctor()
stloc.0 

var res1 = t.ReturnString(10, true);

ldloc.0 
ldc.i4.s 10
ldc.i4.1 
callvirt instance string ConsoleApplication1.Test::ReturnString(int32, bool)
stloc.1 

var res2 = t.ReturnString(new Fun());

ldloc.0
newobj instance void ConsoleApplication1.Fun::.ctor()
callvirt instance string ConsoleApplication1.Test::ReturnString(class ConsoleApplication1.Fun)
stloc.2 

Istotne fragmenty zaznaczyłem na czerwono. ldloc.0 odpowiada za wrzucenie na stos obiektu, na rzecz którego zostanie wywołana metoda (this). Potem wrzucamy argumenty dla wołanej metody czyli ldc.i4.s 10 oraz ldc.i4.1. Po wywołaniu metody zdejmujemy natomiast wynik ze stosu i zapisujemy w zmiennej lokalnej o indeksie 1 przy pomocy komendy stloc.1. Dla drugiego wywołania wygląda to podobnie. Polecenie newobj tworzy nowy obiekt i umieszcza referencję do niego na stosie.

Czemu jednak CLR został zaimplementowany jako automat stosowy (maszyna wirtualna Java zresztą też), a rejestry pojawiają się dopiero kiedy program jest wykonywany i IL jest zamieniany na kod natywny? Otóż CLR to komercyjna implementacja VES (ang. Virtual Execution System). VES jest częścią standardu ECMA-335, który jest niezależny od docelowej architektury, a w szczególności nie wspomina nic o żadnych rejestrach. Dzięki temu konkretne implementacji standardu mogą używać rejestrów w dowolny sposób.

I tutaj dochodzimy do odpowiedzi "nisko poziomowej" na postawione pytanie, czyli jakiej konwencji używa kod natywny wygenerowany na podstawie IL dla Windows'a. Otóż jest to konwencja fastcall, w której na przykład dla architektury x86 dwa pierwsze parametry przekazywane są w rejestrach ECX oraz EDX. Więcej szczegółów na ten temat można znaleźć na blogu Joe Duffy'ego.

Od siebie dodam, że jeśli korzystamy z P/Invoke to domyślnie metody natywne wołane są przy pomocy konwencji Winapi, która w systemie Windows jest tożsama z Stdcall. Zachowanie to możemy zmienić przy pomocy właściwości CallingConvention atrybutu DllImportAttribute.

25/02/2013

Coursera

Home

Ponad rok temu pisałem o możliwości wzięcia udziału w kursach online dotyczących uczenia maszynowego czy baz danych prowadzonych przez Uniwersytet Stanforda. Kursy te okazały się strzałem w dziesiątkę i na przykład na ten dotyczący uczenia maszynowego zapisało się około 100 tysięcy osób!

Wracam do tego tematu ponieważ ostatnio kolega wspomniał mi o projekcie/firmie Coursera, która oferuje każdemu, bezpłatnie, możliwość wzięcia udziału w ponad 300 kursach online, z różnych dziedzin, które zostały przygotowane przez uczelnie z całego świata. Interesują cię kompilatory? A może gra na gitarze lub system monetarny Chin? Każdy powinien znaleźć coś dla siebie. Projekt dopinguję tym bardziej, że założycieli jest profesor nadzwyczajny z Uniwersytetu Stanforda Andrew Ng, który przygotował i poprowadził wspomniany kurs uczenia maszynowego.

Na bieżącą chwilę Coursera ma już 2 mln 700 tysięcy użytkowników. Nieźle jak na projekt, który wystartował niecały rok temu. W każdym razie uważam, że to wspaniała inicjatywa, tym bardziej warta docenienia, że daje dostęp do rzetelnej wiedzy za darmo, co w obecnych czasach jest coraz mniej oczywiste. Żeby tylko znaleźć na to czas ;)

16/02/2013

Co to jest dobre oprogramowanie?

Home

Pytanie jak powyżej pojawiło się w rozmowie w jakiej ostatnio uczestniczyłem. Każdy z nas ma na pewno swoje zdanie na temat cech, które powinien posiadać dobry program/system. Nie inaczej było w tym przypadku i padło wiele odpowiedzi. Po bliższym przyjrzeniu się, okazało się jednak, że odpowiedzi te można pogrupować w raptem cztery kategorie, co było dla mnie pewnym zaskoczeniem. Dodam, że w rozmowie uczestniczyło pięć osób, wszystkie techniczne, ale o różnym doświadczeniu, które pracowały przy różnych projektach i w różnych firmach. Te cztery kategorie to począwszy od najpopularniejszej:
  • Program/system powinien być łatwy oraz tani w utrzymaniu i rozwoju. Do tej kategorii można wrzucić bardzo wiele rzeczy: odpowiednia architektura, obecność testów jednostkowych, dobra dokumentacja wymagań, stosowanie wzorców projektowych itd. Pojawienie się tej kategorii jednoznacznie wskazuje na programistyczny background osób biorących udział w rozmowie.
  • Program/system powinien spełniać wymagania i oczekiwania klienta, rozwiązywać jego problemy.
  • Program/system powinien spełniać wymagania i oczekiwania użytkownika, być ergonomiczny i łatwy w użyciu. Kategoria ta może wydawać się podobna do drugiej kategorii, ale kładzie nacisk na użytkownika końcowego, który w wielu przypadkach nie jest tożsamy z zamawiającym oprogramowanie (klientem).
  • Program/system powinien przynosić firmie tworzącej oprogramowanie zyski, dobrze się sprzedawać. Bez tego też nie da się obejść. Nawet jeśli oprogramowanie zostało napisane z zachowaniem wszystkich zasad i jest cudeńkiem inżynierii, ale się nie sprzedaje, to nie można nazwać go dobrym.
Sądzę, że zestawienie to, pomimo, że krótkie, jest kompletne. Udało się w nim uchwycić interesy różnych grup ludzi, którzy uczestniczą w przedsięwzięciu informatycznym czyli: firmy informatycznej, zespołu tworzącego produkt, klienta/firmy zamawiającej oraz użytkowników końcowych. Interesy tych czterech grup są nierzadko ze sobą sprzeczne ale dopiero kompromis pomiędzy nimi pozwala stworzyć naprawdę dobry system informatyczny.

Po zakończeniu tego posta zacząłem się zastanawiać ile projektów, o których słyszałem, rozmawiałem lub przy których pracowałem spełnia powyższe wymagania. W większości przypadków, które przyszły mi do głowy, mogę gdzieś wsadzić szpilkę. A to system co prawda przynosi kupę kasy ale jest napisany w taki sposób, że woła o pomstę do nieba... Inny z kolei to kawał dobrej programistycznej roboty, ale z jego urynkowieniem jest już gorzej... O wiele łatwiej jest mi znaleźć oprogramowanie wystarczająco dobre, czyli spełniające tylko niektóre z tych wymagań.

A jakie są Wasze doświadczenia?

05/02/2013

WolframAlpha

Home

WolframAlpha to ambitny projekt stworzenia silnika, który umiałby odpowiadać na pytania wyrażone w języku naturalnym, a co więcej odpowiedzią nie byłaby sucha lista stron w Internecie, ale zbiór faktów stanowiących odpowiedź na pytanie. O tym przedsięwzięciu wiedziałem już od dłuższego czasu, ale traktowałem je raczej jako nowinkę i go nie używałem.

Ostatnio kolega zwrócił mi jednak uwagę na możliwość rozwiązywania równań przy pomocy tego silnika. Od tej pory przestałem traktować go tylko jako ciekawostkę i jest pod ogromnym wrażeniem tego projektu, zastosowanych w nim algorytmów, no i oczywiście umiejętności programistów.

Może mały przykład. Jakiś czas temu inny kolega wysłał mail'a z zaproszeniem do poczęstunku z okazji urodzin, ale swój wiek podał w taki oto sposób:



Prawa część równania nie stworzy chyba nikomu problemów. Gorzej jednak z tą całką i sumą szeregu matematycznego, chociaż wyglądają znajomo. Kiedy byłem świeżo po kursie analizy matematycznej pewnie obudzony w środku nocy, po libacji alkoholowej, podałbym wynik bez zastanowienia ;) Teraz jednak, zamiast przypominać sobie wzory, wklepałem to równanie do WolframAlpha:

sum x = 1 to infinity 1/x^2)/(integral from -1 to 1 (1-x^2)^0.5)^2*(2^2^3 - 2^2^2)/2^2

i w mgnieniu oka uzyskałem wynik. Silnik potrafi również rysować wykresy, rozpoznaje rodzaje funkcji np.: powie, że zadane równanie to paraboloida, liczy pochodne, wyznacza nie tylko wartość całki oznaczonej, ale również potrafi policzyć całkę nieoznaczoną, rozwiązuje równania różniczkowe oraz robi pewnie setki innych rzeczy, o których jeszcze nie mam pojęcia. W każdym razie WolframAlpha na stałe zagości w moim przyborniku.

16/01/2013

Mono C# compiler as a service 3

Home

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:

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.

13/01/2013

Mono C# compiler as a service 2

Home

Dzisiaj wrócę do tematu użycia języka C# jako języka skryptowego przy pomocy Mono.CSharp.dll i opiszę w jaki sposób przekazać parametry do takiego skryptu. Pominę podejście opierające się o wklejanie do skryptu string'owej reprezentacji takich parametrów i od razu przejdę do bardziej eleganckiego rozwiązania. Bazuje ono na tym co przeczytałem w tym poście.

Zaczynamy od utworzenia statycznej klasy ScriptContext, która posłuży nam do wymiany danych pomiędzy skryptem, a naszym programem.

namespace Scripting
{
    public static class ScriptContext
    {
        static ScriptContext()
        {
            Items = new Dictionary<string, object>();
        }

        public static object Result { get; set; }

        public static IDictionary<string, object> Items { get; set; }
    }
}

Użycie jej nie jest trudne. Mały przykład:

var settings = new CompilerSettings();
var ev = new Evaluator(new CompilerContext(settings, new ConsoleReportPrinter()));
            
//Informujemy silnik skryptowy gdzie została zdefiniowana klasa ScriptContext
ev.ReferenceAssembly(typeof(ScriptContext).Assembly);

//Ustawiamy parametry
ScriptContext.Items["param1"] = new List<string> { "Hello!", "Welcome!", "Hi!" };

ev.Run("using System;");
ev.Run("using Scripting;");
//Korzystamy z przekazanych parametrów
ev.Run("foreach(var s in (System.Collections.Generic.IEnumerable<string>)ScriptContext.Items[\"param1\"]) Console.WriteLine(s);");

Właściwość ScriptContext.Result dodałem aby nie musieć zastanawiać się kiedy wywołać metodę Evaluator.Run, a kiedy Evaluator.Evaluate. Zawsze używam tej pierwszej, zakładając, że wynik ze skryptu zwracany jest przy użyciu tej właściwości np.:

ev.Run("ScriptContext.Result = 1;");
Console.WriteLine(ScriptContext.Result);

Do szczęścia brakowało mi jednak jeszcze jednej rzeczy. Jak pokazuje pierwszy przykład musiałem użyć rzutowania do IEnumerable<string> aby cieszyć się silnie typowanym parametrem. Napisałem więc metody pomocnicze, które przygotowują silnie typowane parametry:

public static class ScriptingHelper
{
 public static StringBuilder PrepareParameters(IDictionary<string, object> parameters)
 {
  ScriptContext.Items = parameters;

  var sb = new StringBuilder();

  if (parameters != null)
  {
   foreach (var kvp in parameters)
   {
    var typeName = ExtractTypeName(kvp);

    sb.AppendFormat("var {0} = ({1}){2}.Items[\"{0}\"];", kvp.Key, typeName, typeof(ScriptContext).Name);
    sb.AppendLine();
   }
  }

  return sb;
 }

 private static string ExtractTypeName(KeyValuePair<string, object> kvp)
 {
  if (kvp.Value == null)
   throw new Exception("null parameters are not supported!");
   
  var type = kvp.Value.GetType();

  using (var provider = new CSharpCodeProvider())
  {
   var typeRef = new CodeTypeReference(type);
   return  provider.GetTypeOutput(typeRef);
  }
 }
}

Nic bardzo skomplikowanego ale warto zwrócić uwagę na użycie klasy CSharpCodeProvider. Dzięki niej uzyskuję poprawną, pełną nazwę typów, również tych generycznych. Gdybym na przykład dla listy int'ów spróbował użyć czegoś w rodzaju list.GetType().Name to w wyniku otrzymałbym List`1, co do rzutowania się nie nadaje. Wcześniejszy przykład można więc napisać teraz w taki sposób:

//Ustawiamy parametry
var parameters = ScriptingHelper.PrepareParameters(new Dictionary<string, object>
{
 {"param1", new <string> { "Hello!", "Welcome!", "Hi!" } }
});

ev.Run("using System;");
ev.Run("using Scripting;");
ev.Run(parameters.ToString());
//Korzystamy z przekazanych parametrów ale już bez rzutowania
ev.Run("foreach(var s in param1) Console.WriteLine(s);");

Wracając do mojej aplikacji, w której chciałem umożliwić użytkownikowi definiowanie własnych algorytmów obliczania odległości między wektorami. Dane wejściowe to oczywiście dwa wektory. Dzięki takiemu podejściu, zamiast zmuszać użytkownika to rzutowania parametrów na określony typ, używam konwencji czyli: wektor numer 1 znajduje się w zmiennej vector1 itd.

Klasa ScriptingHelper jest również doskonałym miejsce na dodawanie różnych innych "przydasiów" ułatwiających pracę użytkownikowi.

12/01/2013

Mono C# compiler as a service

Home

Od jakiegoś czasu pracuję nad aplikacją do generowania i analizowania wykresów rekurencyjnych. Temat sam w sobie jest bardzo ciekawy, więc może do niego wrócę w przyszłości, ale dzisiejszy post będzie o czymś innym.

Moja aplikacja między innymi wykonuje obliczenia na wektorach np.: oblicza różne odległości (euklidesową, Manhattan czy normę maksimum) między nimi. Dodawanie kolejnych algorytmów wymagało jednak każdorazowej rekompilacji aplikacji. Zacząłem więc szukać sposobu aby umożliwić użytkownikowi dodawanie własnych algorytmów w sposób dynamiczny. Innymi słowy potrzebowałem jakiegoś silnika skryptowego.

Początkowo pomyślałem o PowerShell'u, rozważałem również IronPython'a. Trafiłem jednak na krótki i treściwy post na temat biblioteki Mono.CSharp.dll, która stanowi część dobrze znanego projektu Mono. Opis wyglądał obiecująco dlatego postanowiłem wypróbować tą bibliotekę i był to strzał w dziesiątkę.

Jądrem biblioteki jest klasa Mono.CSharp.Evaluator, która służy do dynamicznego kompilowania i wykonywania kodu C#. Jej użycie jest wręcz trywialne. Oto prosty przykład Hello World!:

var settings = new CompilerSettings();
var ev = new Evaluator(new CompilerContext(settings, new ConsoleReportPrinter()));

ev.Run("using System;");
ev.Run("Console.WriteLine(\"Hello World!\");");

Użycie ConsoleReportPrinter powoduje, że wszelkie komunikaty kompilatora zostaną wypisane na ekran konsoli. Możemy też użyć klasy StreamReportPrinter, wtedy komunikaty zostaną zapisane do wskazanego strumienia. Klasa CompilerSettings pozwala natomiast skonfigurować różne aspekty pracy kompilatora. Kolejny prosty przykład pokazuje, jak zwrócić wynik ze skryptu:

ev.Run("Console.Write(\">\")");
ev.Run("var s = Console.ReadLine();");
var s = (string)ev.Evaluate("s;");

Ciekawe jest to, że możemy zmienić typ raz zadeklarowanej zmiennej np.:

ev.Run("int s = 1;");
ev.Run("Console.WriteLine(s);");
ev.Run("string s = \"Hello!\";");
ev.Run("Console.WriteLine(s);");
ev.Run("bool s = true;");
ev.Run("Console.WriteLine(s);");

Możliwe jest również zdefiniowanie klasy:

ev.Run("class Test { public string Fun(int i) { return \"Hello \" + i; } }");
ev.Run("Console.WriteLine(new Test().Fun(1));");

Załóżmy, że w oddzielnym projekcie mamy zdefiniowaną klasę Test2. Poniższy przykład pokazuje jak użyć jej w skrypcie:

ev.ReferenceAssembly(typeof(Test2).Assembly);
ev.Run("using TestLib;");
ev.Run("Console.WriteLine(new Test2().Welcome(\"Kate\"));");

Praca z klasą Mono.CSharp.Evaluator bardzo mi się podoba. To kawał dobrej roboty, zachęcam więc do wypróbowania. W kolejnych postach opiszę, jak w elegancki sposób przekazać argumenty do skryptu oraz kiedy warto skorzystać z metody Compile zamiast Run lub Evaluate.

Uwaga instalacyjna

Pisząc ten post korzystałem z wersji beta 3.0.3 Mono. Ostatnia dostępna stabilna wersja 2.10.9 zawiera bowiem błąd, który objawia się komunikatem:

Method 'Mono.CSharp.Location.ToString()' is security transparent, but is a member of a security critical type.

przy pierwszej próbie użycia klasy Evaluator.  Nie sprawdzałem ale możliwe, że z github'a można ściągnąć wersję bez tego błędu.