Zewnętrzne aliasy – extern alias

Dzisiaj mało znana funkcjonalność języka\kompilatora, która niestety czasami przydaje się. W zdecydowanej większości przypadków nie powinna ona mieć w projekcie zastosowania, ale wiadomo, czasami nie mamy na to wpływu.

Problem dotyczy scenariusza, gdy mamy dwa typy o takiej samej nazwie, znajdujące się w takiej samej przestrzeni nazw. Oczywiście taka sytuacja wymaga, aby podane typy były umieszczone w różnych projektach (inaczej nastąpiłby błąd kompilacji). Przy wielkich systemach, w których nie przestrzega się dobrych praktyk dotyczących nazewnictwa i podziału na namespace, może okazać się, że podanych typów nie będzie można bezpośrednio użyć w projekcie. Załóżmy, że w projektach Lib1 oraz Lib2 mamy następującą klasę:

public class Type
{
}

Oczywiście w praktyce, klasy w Lib1 i Lib2 miałby zupełnie inną implementację – to tylko uproszczenie. Następnie w jakimś projekcie, dodajemy referencje zarówno do Lib1 jak i Lib2. Struktura całej solucji wygląda więc następująco:

image

Co się stanie jeśli będziemy chcieli użyć Type,  np.:?

Type type=new Type();

Oczywiście nastąpi błąd kompilacji:

Error    1    The type 'Type' exists in both 'Debug\Lib1.dll' and 'Debug\Lib2.dll'

Do rozwiązywania takich konfliktów służą zewnętrzne aliasy. W pierwszej kolejności otwieramy okienko “Properties” dla referencji i ustawiamy kolejno aliasy dla Lib1 oraz Lib2:

image

Domyślna wartość to global. Niestety zarówno Lib1.Type jak i Lib2.Type były ustawione na global, stąd niemożliwe było korzystanie z klas. Dla tego przykładu ustawmy aliasy kolejno na Lib1Test oraz Lib2Test.

Czy to znaczy, że teraz można do typów dostać się za pomocą Lib1Test::Type, podobnie jak było to z global::Type?

Tak, ale najpierw musimy podlinkować wspomniane aliasy za pomocą extern alias:

extern alias Lib1Test;
extern alias Lib2Test;

Powyższe deklaracje powinny znajdować się na samej górze pliku. Dzięki temu, inicjalizacja obiektu wygląda następująco:

Lib1Test::Type type1=new Lib1Test::Type();
Lib2Test::Type type2 = new Lib2Test::Type();

Mam nadzieję jednak, że nikt z Was nie będzie musiał korzystać z takich hack’ów…

Podstawy .NET: dwuetapowa kompilacja i WinDbg do analizy JIT

Tak naprawdę, moje posty powinienem rozpocząć od wyjaśnienia czym jest JIT. To jedno z podstawowych pojęć, które pojawia się w przypadku omawiania .NET. Wolałem jednak najpierw pokazać kilka programów napisanych w IL Assembly.  Kilka postów powinno dać już jakiś obraz czym jest IL.  Oczywiście kod piszemy w C# lub w innym języku wysokiego poziomu, więc wystarczy abyśmy ogólnie mieli pojęcie o IL.

Co to jest więc kompilacja JIT? Czym różni się od klasycznej? W językach niezarządzanych takich jak CPP, kompilator wygeneruje kod maszynowy. W świecie .NET kompilacja jest dwuetapowa. Używając Visual Studio nie generujemy kodu maszynowego, a właśnie IL – język pośredni, który na początku jest kompletnie bezużyteczny. Pomimo, że przypomina on kod maszynowy, to nie można go uruchomić na żadnej maszynie bez wcześniejszego przetworzenia.

Wspomniane “przetworzenie” to kolejna kompilacja, a mianowicie JIT (Just in Time).  W większości przypadków odbywa się w czasie rzeczywistym, w momencie pierwszego wywołania danej metody. Nie JIT’ujemy całej aplikacji od razu bo byłoby to zbyt wolne. Jeśli  w ogóle nie wywołamy jakieś metody, po prostu nie zostanie wygenerowany kod maszynowy dla niej. JIT zatem, to drugi etap kompilacji, który wygeneruje już konkretny kod maszynowy, bardzo często specyficzny dla konkretnej architektury. Ma to wiele zalet, takich jak niezależność od CPU i możliwość optymalizacji kodu dla konkretnej konfiguracji.

Naturalne jest, że JIT dodaje pewien overhead do aplikacji. Wywołanie każdej metody pierwszy raz jest wolniejsze niż kolejne. Z tego względu, jeśli piszemy jakieś benchmarki, musimy mieć to na uwadze, że wyniki mogą być sfałszowane przez JIT. Istnieje możliwość z JIT’owania całej aplikacji przed uruchomieniem programu, ale o tym kiedy indziej. Polecam również mój stary wpis o tzw. MultiCore JIT, który został dodany w .NET 4.5 i ma na celu przyśpieszenie JIT poprzez zrównoleglenie kompilacji.

Podkreślę jeszcze raz, że drugi etap komplikacji (JIT) w przeciwieństwie do pierwszego (Visual Studio, kompilator C#), nie jest wykonywany dla całej aplikacji, a wyłącznie dla konkretnej metody. Po prostu dzieje się to w czasie rzeczywistym i nie ma tyle czasu, aby analizować cały kod. JIT zatem nie dysponuje informacjami o całym kodzie i może wykonać optymalizacje dotyczące wyłącznie kawałka kodu ( w przeciwieństwie do komplikacji c#). Prawdziwa optymalizacja wynika jednak z faktu, że JIT bierze pod uwagę architekturę komputera (CPU) i może wygenerować instrukcje maszynowe , które teoretycznie powinny być optymalne.

Większość książek zawiera powyższe teoretyczne informacje i z tego względu, lepiej popraktykować tutaj. Posłużę się tutaj narzędziem windbg.exe. Jeśli nie pracowaliście z tym debuggerem to polecam MSDN. Windbg.exe jest bardzo przydatny w środowisku produkcyjnym, gdzie nie mamy kodu źródłowego i tym bardziej Visual Studio. Narzędzie jest dość trudne w obsłudze, ale przydatne w eksperymentowaniu oraz analizowaniu błędów, które zdarzyły się na produkcji (np. poprzez memory dump).

image

Nasz eksperyment przeprowadzimy na:

class Program
{
   static void Main(string[] args)
   {
       Display("1");
       Console.ReadLine();
       Display("2");
   }

   private static void Display(string text)
   {
       Console.WriteLine(text);
   }
}

Wywołujemy tam dwukrotnie Display. Za pierwszym razem powinna zostać dokonana kompilacja JIT, a przy drugim wywołaniu, będziemy już dysponować kodem maszynowym.

Pierwszy etap kompilacji, który wygeneruje IL jest prosty i wykonujemy go np. bezpośrednio z Visual Studio. Poskutkuje to wygenerowanym plikiem EXE:

image

Windbg, który stanowi część Windows SDK, jest ogólnym narzędziem do debuggowania. Domyślnie nie wie nic o CLR i zarządzanym kodzie. Z tego względu, musimy zainstalować dodatkowe rozszerzenie SOS.DLL. Aby załadować SOS wystarczy wykonać polecenie:

.load C:\Windows\Microsoft.NET\Framework\v4.0.30319\sos.dll

Kolejna komenda to:

sxe ld:clrjit

Potrzebujemy jej, aby debugger został powiadomiony, gdy CLRJIT jest załadowany.

Następnie wpisujemy g, aby kontynuować:

g

Dzięki poprzedniemu poleceniu zobaczymy:

0:000> g
(1134.17d8): Unknown exception - code 04242420 (first chance)
ModLoad: 730f0000 7316d000   C:\Windows\Microsoft.NET\Framework\v4.0.30319\clrjit.dll
eax=00000000 ebx=00800000 ecx=00000000 edx=00000000 esi=00000000 edi=7f06e000
eip=7721cfbc esp=00bce5dc ebp=00bce638 iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
ntdll!NtMapViewOfSection+0xc:
7721cfbc c22800          ret     28h

Ustawmy breakpoint na metodzie Display za pomocą:

!bpmd ConsoleApplication12.exe ConsoleApplication12.Program.Main

Mamy dokładnie jedną metodę o takiej nazwie więc debugger wyświetli:

Found 1 methods in module 00cc2ed4...
MethodDesc = 00cc37a8
Adding pending breakpoints...

00cc37a8 to identyfikator metody. Dzięki niemu możemy wyświetlić IL wpisując polecenie:

!dumpil 00cc37a8

Wynik:

IL_0000: nop 
IL_0001: ldstr "1"
IL_0006: call ConsoleApplication12.Program::Display
IL_000b: nop 
IL_000c: call System.Console::ReadLine 
IL_0011: pop 
IL_0012: ldstr "2"
IL_0017: call ConsoleApplication12.Program::Display
IL_001c: nop 
IL_001d: ret 

Taki sam kod IL zobaczymy oczywiście w Reflector. Nie powinno być dla nas zaskoczeniem, że pierwszy etap kompilacji został wykonany – mamy w końcu EXE. Naszym celem jest udowodnienie, że JIT nie został jeszcze wykonany, ponieważ metoda nie została wywołana.

Kolejna komenda, wyświetli kod maszynowy:

!u 00cc37a8

Wynik:

00a30050 55              push    ebp
00a30051 8bec            mov     ebp,esp
00a30053 83ec08          sub     esp,8
00a30056 33c0            xor     eax,eax
00a30058 8945f8          mov     dword ptr [ebp-8],eax
00a3005b 894dfc          mov     dword ptr [ebp-4],ecx
00a3005e 833d78318e0000  cmp     dword ptr ds:[8E3178h],0
00a30065 7405            je      00a3006c
00a30067 e8a4c2c673      call    clr!JIT_DbgIsJustMyCode (7469c310)
00a3006c 90              nop

c:\Users\Piotr\Documents\Visual Studio 2013\Projects\ConsoleApplication12\ConsoleApplication12\Program.cs @ 13:
00a3006d 8b0d88214603    mov     ecx,dword ptr ds:[3462188h] ("1")
00a30073 ff15b0378e00    call    dword ptr ds:[8E37B0h] (ConsoleApplication12.Program.Display(System.String), mdToken: 06000002)
00a30079 90              nop

c:\Users\Piotr\Documents\Visual Studio 2013\Projects\ConsoleApplication12\ConsoleApplication12\Program.cs @ 14:
00a3007a e8b1a71b73      call    mscorlib_ni+0xa0a830 (73bea830) (System.Console.ReadLine(), mdToken: 06000995)
00a3007f 8945f8          mov     dword ptr [ebp-8],eax
00a30082 90              nop

c:\Users\Piotr\Documents\Visual Studio 2013\Projects\ConsoleApplication12\ConsoleApplication12\Program.cs @ 15:
00a30083 8b0d8c214603    mov     ecx,dword ptr ds:[346218Ch] ("2")
00a30089 ff15b0378e00    call    dword ptr ds:[8E37B0h] (ConsoleApplication12.Program.Display(System.String), mdToken: 06000002)
00a3008f 90              nop

c:\Users\Piotr\Documents\Visual Studio 2013\Projects\ConsoleApplication12\ConsoleApplication12\Program.cs @ 16:
00a30090 90              nop
00a30091 8be5            mov     esp,ebp
00a30093 5d              pop     ebp
00a30094 c3              ret

Proszę zwrócić uwagę na wywołanie metody Display:

00a30073 ff15b0378e00    call    dword ptr ds:[8E37B0h] (ConsoleApplication12.Program.Display(System.String), mdToken: 06000002)

Ustawiając teraz breakpoint na Display dostaniemy:

0:000> !bpmd ConsoleApplication12.exe ConsoleApplication12.Program.Display
Found 1 methods in module 004f2ed4...
MethodDesc = 004f37b4
Adding pending breakpoints...

Oznacza to, że 004f37b4 jest identyfikatorem Display. Spróbujmy wywołać !u 004f37b4, aby otrzymać kod:

0:000> !u 004f37b4
Not jitted yet

Widzimy wyraźnie, że jest to niemożliwe – drugi etap kompilacji Display nie nastąpił jeszcze. Jeśli pozwolimy na pierwsze wykonanie tej metody (polecenie “g”) to będziemy mogli zobaczyć kod za pomocą tego samego polecenia (!u 004f37b4):

005800d8 55              push    ebp
005800d9 8bec            mov     ebp,esp
005800db 50              push    eax
005800dc 894dfc          mov     dword ptr [ebp-4],ecx
005800df 833d78314f0000  cmp     dword ptr ds:[4F3178h],0
005800e6 7405            je      005800ed
005800e8 e823c21174      call    clr!JIT_DbgIsJustMyCode (7469c310)
005800ed 90              nop

c:\Users\Piotr\Documents\Visual Studio 2013\Projects\ConsoleApplication12\ConsoleApplication12\Program.cs @ 24:
005800ee 8b4dfc          mov     ecx,dword ptr [ebp-4]
005800f1 e84601fa72      call    mscorlib_ni+0x34023c (7352023c) (System.Console.WriteLine(System.String), mdToken: 060009a3)
005800f6 90              nop

c:\Users\Piotr\Documents\Visual Studio 2013\Projects\ConsoleApplication12\ConsoleApplication12\Program.cs @ 25:
005800f7 90              nop
005800f8 8be5            mov     esp,ebp
005800fa 5d              pop     ebp
005800fb c3              ret

Możemy również napisać mały benchmark, aby pokazać, że faktycznie zachodzi JIT:

class Program
{
   static void Main(string[] args)
   {
       Stopwatch stopWatch = Stopwatch.StartNew();
       Display("1");
       Console.WriteLine(stopWatch.ElapsedTicks);
       
       stopWatch.Restart();
       Display("2");
       Console.WriteLine(stopWatch.ElapsedTicks);
   }

   private static void Display(string text)
   {
       Console.WriteLine(text);
   }
}

Na ekranie pojawi się:

image

Widać, że drugie wywołanie jest wyraźnie szybsze ponieważ kod jest już po JIT.

Polecam zapoznanie się z narzędziem WinDbg, szczególnie dla osób, które muszą naprawiać błędy występujące wyłącznie na produkcji lub w innym specyficznym środowisku.

Visual Studio–kilka skrótów ułatwiających pracę

1. Załóżmy, że mamy klasę z wieloma polami, np.:

public struct Employee
{
   internal string Field1;
   internal string Field2;
   internal string Field3;
   internal string Field4;
   internal string Field5;
   internal string Field6;
}

Następnie chcemy zmienić modyfikator internal na public. Naturalne podejście byłoby zmodyfikowanie wszystkich wywołań jeden po drugim. Inne rozwiązanie to CTRL+H i automatycznie zastąpienie. W zależności od konkretnej klasy, może być to wygodne lub nie. Trzeba być uważnym jednak, aby wszystkich wystąpień w pliku nie zamienić(jeśli np. chcemy tylko kilka z nich zaktualizować).

W VS możemy nacisnąć klawisz ALT, który umożliwi nam pionowe zaznaczenie tzn.:

image

Następnie możemy zmodyfikować tekst do public i zostanie to odzwierciedlone we wszystkich zaznaczonych liniach tzn.:

image

To jest dość stara funkcja Visual Studio, ale niezbyt popularna, a czasami potrafi ułatwić życie.

2. Jeśli użyjemy CTRL, a potem – (myślnik), to kursor zostanie przeniesiony do ostatnio używanej linii Przydatne, gdy skaczemy gdzieś dalej w pliku (aby zmodyfikować np. parametr wejściowy), a potem chcemy z powrotem powrócić bez używania myszki. Analogicznie możemy skorzystać z kombinacji CTRL, Shift, – aby przejść do przodu.

3. Generalnie zasada jest taka, że im mniej używamy myszki, tym szybciej jesteśmy w stanie pisać kod. Ciekawym skrótem jest CTRl+X, który usuwa całą linie. Jeśli zaznaczymy ręcznie linie, to usunięta zostanie wyłącznie sama linia, bez znaku nowej linii. Na przykład, jeśli mamy kursor na polu Field3 to naciśnięcie CTRL+X, spowoduje usunięcie go wraz ze znakiem nowej linii, co poskutkuje:

public struct Employee
{
   public string Field1;
   public string Field2;
   public string Field4;
   public string Field5;
}

Jeśli z kolei cała linia byłoby zaznaczona, to znak nowej linii zostanie pozostawiony bez zmian:

public struct Employee
{
   public string Field1;
   public string Field2;

   public string Field4;
   public string Field5;
}

4. Jeśli chcemy przejść do innego aktywnego pliku (bez używania myszki) możemy użyć kombinacji CTRL+Tab, co wyświetli następujące okno:

image

5. Shift+Alt+Enter – szybkie przełączanie się między FULLSCREEN a normalnym trybem. Przydatne, gdy mamy dużo kodu i nie chcemy szukać tej opcji w menu.

6. Jeśli chcemy przesunąć daną linię w dół albo w górę również nie musimy używać myszki. Wystarczy nacisnąć alt i strzałkę w górę (up) lub w dół (down).

7. Zoom chyba jest dość oczywisty w VS. Wystarczy nacisnąć ctrl i poruszać kółkiem myszki, a rozmiar czcionki automatycznie będzie modyfikowany (tak jak to w większości przeglądarkach internetowych).

image

image

8. Pamiętam, że kiedyś aby sformatować kod w VS, używałem zwykle zamknięcia klamr itp. Na szczęście istnieje skrót, który to zrobi dla nas w dowolnym momencie. Załóżmy, że mamy źle sformatowany kod tzn.:

image

W menu głównym możemy przejść do Edit->Advanced->Format Document:

image

Warto również zapamiętać pokazane skróty klawiszowe – klikanie po menu jest zdecydowanie zbyt powolne…

9. Przewijanie bez myszki i zachowanie pozycji kursora – CTRL + UP albo DOWN.

10. Bardzo dużo osób używa resharper’a. Często zamiast korzystać z Solution Explorer, klikamy CTRL+T, aby przejść do konkretnej klasy. Niestety, czasami mamy tak skomplikowaną strukturę folderów, że nie mamy pojęcia gdzie znaleziony plik przez Resharper znajduje się. A co jeśli chcemy przenieść dany plik do innego folderu? Musimy jakąś zlokalizować dany element w Solution Explorer. W VS możemy kliknąć na Sync With Active Document w SE:

image

Mam na myśli tą ikonę dwóch strzałek. Wtedy automatycznie zostaniemy przeniesieni do danego pliku w Solution Explorer:

image

11. Peek Definition. To nowa opcja w VS o której pisałem tutaj. Wspominam o niej ponownie, ponieważ moim zdaniem jest niedoceniana i przez to nie zawsze programiści wyrabiają sobie odpowiedni nawyk z nowym narzędziem.

IL Assembly: Wywoływanie metod

Kiedyś na blogu już wspomniałem o różnych sposobach wywoływania metod przez CLR. Myślę jednak, że warto przypomnieć sobie te informacje jeśli omawiamy już język IL. W IL istnieją trzy sposoby na wykonanie metod:

  1. call
  2. callvirt
  3. calli

Najważniejsze to te dwie pierwsze. Instrukcja callvirt służy, jak sama nazwa wskazuje, do wykonywania metod wirtualnych. Rozważmy przykład:

public class Employee
{
   public virtual void Print()
   {
       
   }
}

public class Manager : Employee
{
   public override void Print()
   {
       base.Print();
   }
}
class Program
{        
   private static void Main()
   {
       Employee employee=new Manager();
       employee.Print();
   }
}

Wygenerowany IL będzie wyglądać następująco:

IL_0001: newobj instance void ConsoleApplication11.Manager::.ctor()
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: callvirt instance void ConsoleApplication11.Employee::Print()

Widzimy, że callvirt jest używany tutaj, aby wykonać metodę Print:

IL_0008: callvirt instance void ConsoleApplication11.Employee::Print()

Jeśli przyjrzymy się powyższej instrukcji, zobaczymy, że w żaden sposób nie przekazuje ona referencji na której chcemy wykonać Print. Wspomniana metoda (Print) nie jest statyczna więc musimy jakoś mieć obiekt this.

Aby to zrozumieć, przejdźmy kilka instrukcji wyżej. Najpierw tworzymy instancje obiektu Manager:

IL_0001: newobj instance void ConsoleApplication11.Manager::.ctor()

Instrukcja newobj umieści nową instancję na stosie (co nie powinno być już zaskoczeniem). Następnie standardowo umieszczamy właśnie stworzoną wartość ze stosu w zmiennej lokalnej, a potem znów ją ładujemy na stos:

IL_0006: stloc.0
IL_0007: ldloc.0

Zmienne lokalne z kolei, zdefiniowane są następująco:

.locals init (
    [0] class ConsoleApplication11.Employee employee
)

Innymi słowy, callvirt weźmie wartość ze stosu i użyje jej jako this. callvirt potrafi wyznaczyć, która metoda powinna być wykonana za pomocą virtual method table. Z tego względu, dla wszystkich metod wirtualnych konieczne jest użycie callvirt a nie call. Instrukcja call nie korzysta z virtual method table, stąd nie wiadomo byłoby, która metoda powinna być wykonana.

Ponadto callvirt wyrzuca kilka intersujących wyjątków:

1. NullReferenceException – sprawdzane jest, czy obiekt na którym wykonujemy metodę ma jakąś wartość. W praktyce oznacza to, że następujący kod wyrzuci NullReferenceException:

Employee employee = null;
employee.Print();

Skutek:

image

Dla większości programistów jest to oczywiste, ale jak zobaczymy później, nie powinno to być takie naturalne, że wywołanie obiektu na NULL wyrzuca wyjątek.

2.  SecurityException – brak uprawnień

3. MissingMethodException

Kolejna instrukcja call służy do wykonania niewirtualnych metod. Klasycznym przykładem są metody statyczne:

public class Employee
{
   public virtual void Print()
   {
       
   }
   public static void StaticPrint()
   {
       
   }
}

class Program
{        
   private static void Main()
   {
       Employee.StaticPrint();
   }
}

IL:

IL_0001: call void ConsoleApplication11.Employee::StaticPrint()

Tutaj bardzo ważna uwaga. Call nie sprawdza czy referencja jest równa NULL. W przypadku metod statycznych, nie ma to znaczenia, ale co z niewirtualnymi instance methods?

C#:

public class Employee
{
   public void InstancePrint()
   {
       
   }
}
class Program
{        
   private static void Main()
   {
       Employee employee = null;
       employee.InstancePrint();
   }
}

IL:

IL_0001: ldnull
IL_0002: stloc.0
IL_0003: ldloc.0
IL_0004: callvirt instance void ConsoleApplication11.Employee::InstancePrint()

I tutaj niespodzianka… Wywołujemy niestatyczną metodę a korzystamy z callvirt. Dlaczego? W końcu nie potrzebujemy korzystać z virtual table method. Twórcy CLR zdecydowali jednak się na taki krok, aby uniknąć sytuacji, gdzie wykonujemy metodę na NULL i nie powoduje to wyjątku. Byłoby to bardzo nieintuicyjne zachowanie. Niestety jednak, istnieją sytuacje, w których nie unikniemy tego problemu.

Metody rozszerzające są klasami statycznymi, a więc będą na pewno wykonane przez call. Niestety operują one czasami na klasycznych obiektach. Może zdarzyć się, że wykonamy metodę na referencji wskazującym na NULL. Przykład:

public class Employee
{
   public string Text = "Hello World";
}

static class EmployeeExtensions
{
   public static void Print(this Employee employee)
   {
       Console.WriteLine("Ten kod wykona sie bez problemu");
       Console.WriteLine(employee.Text);
   }
   
}
class Program
{        
   private static void Main()
   {
       Employee employee = null;
       employee.Print();
   }
}

Powyższy kod wywoła Print bez wyjątku. Nie jest sprawdzane czy obiekt równa się NULL. Dopiero próba dostępu do Text spowoduje wyjątek:

image

Trzeba mieć to na uwadze bo może spowodować to nieoczekiwane efekty w aplikacji.

IL do powyższego kodu:

IL_0000: nop
IL_0001: ldnull
IL_0002: stloc.0
IL_0003: ldloc.0
IL_0004: call void ConsoleApplication11.EmployeeExtensions::Print(class ConsoleApplication11.Employee)
IL_0009: nop
IL_000a: ret

Widać wyraźnie, że call został użyty. Podsumowując, call jest dla statycznych metod a callvirt dla instance-methods (nawet tych niewirtualnych!).

Póki co, zajęliśmy się wyłącznie metodami bez parametrów i wartości zwracanej. Prześledźmy następujący kod:

public class Employee
{
   public string Print(string text)
   {
       Console.WriteLine(text);

       return text;
   }
}

class Program
{        
   private static void Main()
   {
       Employee employee = new Employee();
       employee.Print("Hello World");
   }
}

IL:

IL_0001: newobj instance void ConsoleApplication11.Employee::.ctor()
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: ldstr "Hello World"
IL_000d: callvirt instance string ConsoleApplication11.Employee::Print(string)

Po kolei:

1. 0001 – tworzymy instancję obiektu

2. 0006 – przechowujemy obiekt w zmiennej lokalnej

3. 0007 – umieszczamy zmienną lokalną na stosie

4. 0008 – przechowuje na stosie napis “Hello World”

5. 000d – wywołujemy (callvirt) metodę Print. Na stosie zatem będziemy mieli obiekt this i parametr wejściowy “Hello World”.

Nie powinno budzić wątpliwości fakt, że parametry wejściowe, jak wszystko inne umieszczane są na stosie. Tak samo wynik (wartość zwracana) jest umieszczona na stosie. Powyższy fragment kodu nie pokazuje tego, ale return tak naprawdę umieści argument na stosie, który potem można zdjąć i wyświetlić jak każdą inną wartość.

Przejdźmy teraz do ciała metody Print, aby przekonać się jak argumenty są używane:

IL_0001: ldarg.1
IL_0002: call void [mscorlib]System.Console::WriteLine(string)
IL_0007: nop
IL_0008: ldarg.1
IL_0009: stloc.0
IL_000a: br.s IL_000c

IL_000c: ldloc.0
IL_000d: ret

ldarg załaduje parametr wejściowy o określonym indeksie na stos. Z kolei ret zwraca wartość i wykonanie. Reszta instrukcji powinna być znana (w debug trochę śmieci jest wygenerowanych). Ret zwraca wykonanie, ale sami powinniśmy umieścić zwracaną wartość na stosie (tutaj robimy to za pomocą ldloc.0, w której znajduje się po prostu parametr wejściowy wcześniej załadowany). Gdy funkcja zwraca void, wtedy po prostu nic nie umieszczamy na stosie.

Instrukcja calli jest najrzadziej spotykana i służy do wykonywania metod w sposób pośredni – przez wskaźnik (interop). Innymi słowy,  calli wywołuje metodę, która jest określona jako wskaźnik znajdujący się na stosie. Poprzednie instrukcje wywoływały jawnie konkretne metody.

Klasa ExceptionDispatchInfo w .NET 4.5

W wersji .NET 4.5 pojawiła się klasa ExceptionDispatchInfo, która nie jest zbyt znana. W większości przypadków nie jest na szczęście potrzebna, ale np. w wielowątkowości może ułatwić bardzo specyficzne zadania wyrzucania i filtrowania wyjątków.

Klasyczny przykład użycia to zebranie w kolekcji różnych wyjątków, a następnie wyrzucenie ich ponownie (prawdopodobnie po nałożeniu odpowiedniego filtra) w jakimś innym miejscu. Taki scenariusz to jest dokładnie sytuacja w której klasa została użyta w TPL i w konstrukcji await\async.

Załóżmy, że mamy kilka metod:

private static  void Throw()
{
  Throw2();
}

private static void Throw2()
{
  Throw3();
}

private static void Throw3()
{
  throw new NotImplementedException();
}

Chcemy wywoływać Throw, która następnie wywoła kilka metod pod rząd, wywołując na końcu wyjątek. Sytuacja wielowątkowa może wyglądać następująco:

var exceptions = new ConcurrentBag<ExceptionDispatchInfo>();

ThreadPool.QueueUserWorkItem(_ =>
{
 try
 {
     Throw();
 }
 catch (Exception ex)
 {
     ExceptionDispatchInfo exceptionDispatchInfo = ExceptionDispatchInfo.Capture(ex);
     exceptions.Add(exceptionDispatchInfo);
 }
});

Jak widzimy, w kodzie tylko zbieramy wyjątki i nie wyrzucamy ich od razu. Taka sytuacja ma często miejsce w TPL, na przykład w przypadku równoległych pętli.

ExceptionDispatchInfo ma metodę Capture, która owinie odpowiednio wyjątek. Dzięki temu nie stracimy informacji o StackTrace.

Na końcu, w kodzie zbiorczym, chcemy wyjątek wyrzucić ponownie, ale koniecznie z zachowaniem oryginalnego stackstrace. W praktyce prawdopodobnie dokonujemy w tym miejscu również jakiś filtrów. Najprostszy przykład może wyglądać następująco:

foreach (ExceptionDispatchInfo exceptionDispatchInfo in exceptions)
{

 try
 {
     exceptionDispatchInfo.Throw();
 }
 catch (Exception ex)
 {
     Console.WriteLine("{0}", ex);
 }
}

ExceptionDispatchInfo zatem zawiera dwie najważniejsze metody:

  1. Capture – przechowanie wyjątku
  2. Throw – wyrzucenie go podobnie

Wygenerowany wyjątek będzie zawierał stary (oryginalny) stacktrace:

image

Innymi słowy, jeśli chcemy wyrzucić wyjątek ponownie w innym miejscu, bez modyfikacji StackTrace (np. poprzez WrappedException), możemy skorzystać z przedstawionej klasy.

IL Assembly: instrukcje warunkowe i pętle

Pętle i warunki, naturalnie są z jednym z podstawowych elementów każdego programu. Języki wysokiego poziomu umożliwiają realizację tego za pomocą słów If, for, foreach,while. W językach niskiego poziomu, takich jak IL Assembly, wszystkie powyższe czynności wykonuje się za pomocą skoków warunkowych i bezwarunkowych. Działają one analogicznie do słowa kluczowego GOTO.

W IL, najprostsza instrukcja skoku to BR:

br jump1
// jakas logika tutaj
jump1:
//...

br nie zawiera żadnego warunku. Można ją porównać do goto – po prostu skacze do wskazanej lokalizacji.

Bardziej interesujące są brtrue oraz brfalse. W zależności czy wartość jest true lub false, skaczą do wskazanego miejsca. Rozważmy kod c#:

bool flag = true;
if (flag)
{
 Console.WriteLine("true");
}
else
{
 Console.WriteLine("false");
}

Wygenerowany IL to:

IL_0001: ldc.i4.1
IL_0002: stloc.0
IL_0003: ldloc.0
IL_0004: ldc.i4.0
IL_0005: ceq
IL_0007: stloc.1
IL_0008: ldloc.1
IL_0009: brtrue.s IL_001a

IL_000c: ldstr "true"
IL_0011: call void [mscorlib]System.Console::WriteLine(string)
IL_0018: br.s IL_0027

IL_001a: nop
IL_001b: ldstr "false"
IL_0020: call void [mscorlib]System.Console::WriteLine(string)
IL_0027: ret

Jeśli któraś z instrukcji jest niezrozumiała, zapraszam do poprzednich wpisów o IL na moim blogu. Wszystkie z powyższych instrukcji powinny być już zrozumiałe. Tak czy inaczej, spróbujmy rozszyfrować tą wiązankę.

1. IL_0001 – załadowanie wartości jeden na stos.

2. IL_0002 – zdjęcie wartości (1 – true) ze stosu i umieszczenie jej w zmiennej lokalnej o indeksie zero (czyli flag).

3. IL_003 – załadowanie zmiennej lokalnej na stos.

4. IL_004 – umieszczenie wartości 0 (false) na stosie.

5. IL_005 – ceq to instrukcja opisana w poprzednim wpisie. Porównuje dwie wartości i jeśli są one równe, cyfra 1 jest umieszczana na stosie, w przeciwnym wypadku 0. W naszej sytuacji będzie to porównanie jeden  z zero (1==0) co poskutkuje, że na stosie pojawi się 0.

6. IL_007 – zapisanie wartości ze stosu (w naszym przypadku zero) do zmiennej lokalnej o indeksie 1. Jest to automatycznie wygenerowana zmienna przez kompilator. Zaglądając do deklaracji przekonamy się o tym:

.locals init (
    [0] bool flag,
    [1] bool CS$4$0000
)

7. IL_008 – analogiczna sytuacja – umieszczenie zmiennej na stosie.

8. IL_009 – wreszcie mamy naszą instrukcję warunkową brtrue.s IL_001a. Jeśli wartość na stosie jest true, skoczymy do IL_001a, czyli:

IL_001a: nop
IL_001b: ldstr "false"
IL_0020: call void [mscorlib]System.Console::WriteLine(string)

W naszym przypadku, na stosie jest zero, zatem nie zostanie wykonany skok w tym momencie, a wykonamy:

IL_000c: ldstr "true"
IL_0011: call void [mscorlib]System.Console::WriteLine(string)

IL_0018: br.s IL_0027

Na końcu jednak widzimy br. Oczywiście, chcemy ominąć fragment kodu, który wykonuje gałąź else, stąd skaczemy do IL_007.

Jedyną instrukcją o której nie wspomniałem to nop. Na razie wystarczy wiedzieć, że ona nic nie robi (dosłownie, Do Nothing). W tym przypadku, służy wyłącznie jako miejsce skoku. Więcej o niej napiszę kiedy indziej.

Pozostałe instrukcje skoku to: beq, bne, blt, ble, bgt, bge. Analogicznie, w zależności od wyniku porównania, skaczą do podanego miejsca. Przykład c#:

int five = 5;
int three = 3;

if (five>three)
{
 Console.WriteLine("true");
}
else
{
 Console.WriteLine("false");
}

IL:

IL_0000: ldc.i4.5
IL_0001: stloc.0
IL_0002: ldc.i4.3
IL_0003: stloc.1
IL_0004: ldloc.0
IL_0005: ldloc.1
IL_0006: ble.s IL_0013

IL_0008: ldstr "true"
IL_000d: call void [mscorlib]System.Console::WriteLine(string)
IL_0012: ret

IL_0013: ldstr "false"
IL_0018: call void [mscorlib]System.Console::WriteLine(string)
IL_001d: ret

Warto zauważyć, że przedstawione instrukcje właściwie kończą się sufiksem .s.  Sufiks oznacza short i korzysta się takich instrukcji gdy wskaźnik skoku (przesunięcia) można zapisać za pomocą jednego bajta. Innymi słowy, jeśli nie trzeba skakać zbyt daleko w IL to lepiej zapisać przesunięcie w jednobajtowej liczbie, co jest oszczędnością i wpływa na wydajność.

IL, jako, że jest językiem niskopoziomowym nie posiada specjalnych poleceń dla pętli. Wykonuje się je po prostu za pomocą skoków. Jeśli należy wykonać drugą iterację, skaczę się z powrotem na początek.

Przykład c# while:

int i = 0;

while (i<5)
{
 i++;
}

IL:

IL_0000: nop
IL_0001: ldc.i4.0
IL_0002: stloc.0
IL_0003: br.s IL_000b
// loop start (head: IL_000b)
IL_0005: nop
IL_0006: ldloc.0
IL_0007: ldc.i4.1
IL_0008: add
IL_0009: stloc.0
IL_000a: nop

IL_000b: ldloc.0
IL_000c: ldc.i4.5
IL_000d: clt
IL_000f: stloc.1
IL_0010: ldloc.1
IL_0011: brtrue.s IL_0005
// end loop

IL_0013: ret

Jeszcze kilka ważnych rzeczy z IL mam zamiar opisać w kolejnych postach. Bazując jednak wyłącznie na tym i ostatnich wpisach, wiele można zrozumieć analizując kod z Reflector czy ILSpy.

IL assembly: Załadowanie zmiennych lokalnych + operatory

W poprzednim poście opisałem jak działa stos w IL. Wiemy, że jest on kluczowy dla wszelkich operacji. Jedną z ważniejszych instrukcji, wprowadzonych w tamtym poście jest ldc.i4. Służy ona do załadowania liczby 4 bajtowej na stos.
Oprócz niej, istnieje wiele innych instrukcji, które pełnią analogiczną rolę. Rozważmy następujący kod c#:

float floatNumber = 533.3f;
double doubleNumber = 5454.14;
string text = "text";
int[] array = new int[] {21, 3};

Console.WriteLine(floatNumber);
Console.WriteLine(doubleNumber);
Console.WriteLine(text);

Float zostanie załadowany za pomocą:
ldc.r4 533.3

Instrukcja analogiczna do wspomnianej ldc.i4. Cyfra „4” oznacza liczbę bajtów, a przedrostek „r” lub „i” kolejno real (zmiennoprzecinkowa) i integer (całkowita).

Dla double będzie to ldc.r8, ponieważ jest to liczba 8-bajtowa. String z kolei ma osobną instrukcję ldstr:

ldstr "text"

To jeszcze nie wszystko. W IL istnieje wiele skrótów. Oczywiście programistów c# zwykle takie szczegóły nie interesują. Ułatwia to jednak czytanie kodu IL, kiedy pracujemy nad jakąś optymalizacją albo gdy po prostu aplikacja nie działa poprawnie.

W celu zaprezentowania skrótów do powyższych instrukcji spójrzmy na:

int minusOne = -1;
int plusOne = 1;
bool falseVaue = false;
bool trueValue = true;

object objectNull = null;

Powyższe wartości maja specjalne instrukcje. Zamiast np. pisać ldc.i4 -1 mamy ldc.i4.m1. Podsumowując, powyższy kod wygeneruje kolejno:

•    ldc.i4.m1 – załadowanie -1

•    ldc.i4.1 – załadowanie +1

•    ldc.i4.0 – załadowanie false (Boolean)

•    ldc.i4.1 – załadowanie true (Boolean)

•    ldnull – załadowanie null

W dokumentacji znajdziemy wiele innych skrótów, ale są one analogiczne do powyższych i dość oczywiste.

Myślę, że przy okazji stosu, warto również wspomnień o:

•    pop – instrukcja zdejmuje element ze stosu

•    dup – instrukcja duplikujecie element znajdujący się na górze stosu.

Funkcja pop jest dość oczywista i po prostu usunie wartość ze stosu. Najprostszy kod, obrazujący pop to:

int a = 5;
a.ToString();
int b = 5;

Wywołanie ToString spowoduje umieszczenie wyniku na stosie. Nie jest on nigdzie używany, zatem należy go usunąć potem (przed int b=5) ze stosu:

ldc.i4.5
stloc.0
ldloca.s a
call instance string [mscorlib]System.Int32::ToString()
pop

Instrukcja dup z kolei ma na celu optymalizację. Bardzo często musimy mieć duplikat wartości na stosie. Na przykład, jeśli chcemy tą samą liczbę umieścić w zmiennej lokalnej i wykonać jakaś operacje na niej. Instrukcja stloc (umieszczenie wartości w zmiennej lokalnej) zdejmuje również wartość ze stosu, a wszelkie operacje operują wyłącznie na stosie.

W poprzednim wpisie pokazałem również instrukcje add. Oczywiście istnieje szereg innych operacji tzn.:

  • 1.    neg – negacja (!)
  • 2.    sub – odejmowanie
  • 3.    mul – mnożenie
  • 4.    div – dzielenie
  • 5.    rem – reszta z dzielenia
  • 6.    shl – przesuniecie o jeden bit w lewo
  • 7.    shr- analogicznie w prawo
  • 8.    operatory bitowe: and, or, xor, not

Jeśli mamy dwie wartości (a,b) na stosie, wykonanie odpowiedniej instrukcji umieści cyfrę 1 albo 0, w zależności od operatora i relacji argumentów. Dostępne instrukcje to: clt, cle, cgt, cge, ceq, cne.

Wprowadzenie do evaluation stack

Bardzo często na blogu poruszam tematykę c# internals. Bez nich, praktycznie niemożliwe jest pisanie optymalnego kodu. Jeśli ktoś np. nie wie jak async\await jest zaimplementowany wewnętrznie, bardzo łatwo może popełnić błędy podczas pisania kodu c#. Niedawno ktoś zasugerował mi, abym wyjaśnił bardziej IL. Bardzo często wklejam fragmentu kodu z Reflector’ora i nie wyjaśniam szczegółów.

Z tego względu, przez kilka kolejnych wpisów zajmiemy się CLR internals oraz IL.

Na początek podstawowe pytanie, co to jest IL? Jest to skrót od  Intermediate Language. Kompilując kod C# czy VB, nie otrzymujemy kodu maszynowego, specyficznego dla konkretnej architektury. IL to język pośredni, który zostanie dopiero zamieniony w konkretny kod maszynowy przez JIT. Umożliwia to pisanie kodu, który nie jest zależny od konkretnej platformy\architektury. Dzięki temu również, mogą być dokonywane różne optymalizacje zależne np. od konkretnego CPU.

Bardzo często mylony jest IL z IL Assembly. IL to tak naprawdę kod binarny, którego człowiek nie jest w stanie przeczytać\zrozumieć. Z kolei IL Assembly to język niskiego poziomu (assembler). W skrócie będę pisał na blogu IL pokazując różne fragmenty kodu. Ale należy mieć na uwadze, że tak naprawdę chodzi IL Assembly… Nie da się wykonać bezpośrednio IL Assembly, musi on być potem z powrotem zamieniony do IL.

Dzisiaj zajmiemy jednym z ważniejszych elementów IL, a mianowicie stosem (konkretniej evaluation stack). IL, w przeciwieństwie do wielu innych jeżyków jest oparty na stosie a nie na rejestrach. Wszystkie operacje wykonujemy, umieszczając pewne dane na stosie, a potem wykonując różne instrukcje.

W skrócie, IL nie posiada rejestrów, ale stos jak i m.in. zmienne lokalne. Nie lubię zbyt wiele teorii, więc zacznijmy od przykładu c#:

static  void Main(string[] args)
{
  int a = 5;
  int b = 6;
  int c = a + b;
}

W tym prostym programie, mamy 3 zmienne lokalne. Pierwsza z nich przechowuje stałe a kolejna („c”) będzie zawierać sumę dwóch wcześniejszych zmiennych. Wygenerowany IL Assembly, będzie wyglądać, więc następująco:

.locals init (
   [0] int32 num,
   [1] int32 num2,
   [2] int32 num3)
L_0001: ldc.i4.5 
L_0002: stloc.0 
L_0003: ldc.i4.6 
L_0004: stloc.1 
L_0005: ldloc.0 
L_0006: ldloc.1 
L_0007: add 
L_0008: stloc.2 

Najpierw widzimy deklaracje zmiennych lokalnych:

.locals init (
   [0] int32 num,
   [1] int32 num2,
   [2] int32 num3)

To było proste do wywnioskowania. Kolejne instrukcje, bez znajomości internali są ciężkie prawdopodobnie do rozszyfrowania. Stos służy do wykonywania operacji. Aby dodać dwie liczby, należy je najpierw umieścić na stosie. Analogicznie, nie operuje się bezpośrednio na zmiennych lokalnych. Jeśli chcemy przechować liczbę w zmiennej lokalnej, najpierw należy ją umieścić na stosie, a potem zdjąć ją, umieszczając w konkretnej zmiennej lokalnej.

Spróbujmy przedstawić działanie powyższego kodu. W celu załadowania jakiejkolwiek wartości do zmiennej lokalnej, należy najpierw umieścić ją na stosie. Z tego względu stos na początku będzie wyglądać następująco:

clip_image002

Zmienne lokalne na tym etapie są puste. Powyższą operację, wykonuje się instrukcją ldc.i4 – przechowuje ona liczbę 4-bajtową (Int32) na stosie.

Kolejna instrukcja to stloc.0. Zdejmie ona wartość (5) ze stosu i umieści ją w pierwszej zmiennej lokalnej. Następnie mamy dwie kolejne, analogiczne instrukcje dla cyfry 6. Najpierw umieszczamy ją na stosie, a potem zdejmujemy, wrzucając do zmiennej lokalnej o indeksie jeden:

L_0003: ldc.i4.6 
L_0004: stloc.1 

Stos jest pusty na tym etapie, a dane znajdują się w zmiennych lokalnych (a,b). W celu ich dodania, musimy znów umieścić je na stosie:

L_0005: ldloc.0 
L_0006: ldloc.1 

Ldloc to instrukcja analogiczna do stloc. Jak wiemy, stloc zdejmuje wartość ze stosu, umieszczając ją w zmiennej lokalnej, a z kolei ldloc załaduje zmienną lokalną na stos. Oznacza to, że stos po tych dwóch instrukcjach będzie wyglądać następująco:

clip_image002[5]

Następnie wykonujemy add, która doda dwie wartości ze stosu i przechowa wynik również na nim:

L_0007: add

Co w wyniku da:

clip_image002[7]

Ostatnia linia już nie powinna być zaskoczeniem:

L_0008: stloc.2

Spowoduje, to zdjęcie wartości ze stosu i umieszczenie jej w lokalnej zmiennej o indeksie 2 (czyli zmienna o nazwie c).

Na pierwszy wpis wystarczy… Podsumowując jednak instrukcje z dzisiaj:

  1. stloc – zdejmuje wartość ze stosu i umieszcza ją we wskazanej przez indeks zmiennej lokalnej.
  2. ldloc – załaduje zmienną lokalną o wskazanym indeksie do stosu (push).
  3. add – sumuje dwie liczby (operuje na stosie).
  4. ldc.i4 – ładuje liczbę 4 bajtową (Int32) na stos.