IL Assembly: wywoływanie metod wirtualnych na typach generycznych

Posted September 19th, 2014 by Piotr Zieliński
Categories: C#, Patterns & Practices

Dzisiaj kolejny wpis o generics internals. Wiemy już jak zostały one zaimplementowane w .NET i czym różni się generics z typem referencyjnym od value.  Kolejny etap to zapoznanie się z OpCodes.Constrained. Zobaczmy następujący kod:

class CustomStack<T>
{
   public void DoSomething(T value)
   {
       string str=value.ToString();
   }
}

Prosta metoda, przyjmująca typ generyczny. Dlaczego stanowi ona problem i wyzwanie dla twórców języka?

Jak wiemy, ToString to metoda zaimplementowana w klasie Object. Każdy zatem obiekt posiada ją. Wiemy również, że value types również dziedziczą po object (pośrednio). Nie ma zatem znaczenia czy mamy do czynienia z Integer czy z własną klasą – zawsze mamy do dyspozycji ToString. Oczywiście powoduje to problemy dla value type. Wywołując ToString powodujemy boxing, ponieważ należy skonwertować value type do Object, który jest typem referencyjnym.

Jak zatem zachowa się powyższa metoda? Na etapie kompilacji nie wiadomo czy ciało metody będzie operowało na value czy referencyjnym typie. Wiemy również, że ciało metody nie jest kopiowanie dla każdego typu generycznego a jest współdzielone – wyłącznie layout dla typów value jest różny, a sama logika (ciała metod) są zawsze współdzielone. Z tego względu, jak IL będzie wyglądać? Skąd CLR ma wiedzieć czy dokonać boxingu czy można ToString bezpośrednio wykonać? Zajrzyjmy do IL Assembly. Sama klasa będzie zaprezentowana następująco:

.class private auto ansi beforefieldinit ConsoleApplication16.CustomStack`1<T>
    extends [mscorlib]System.Object
{
    // Methods
    .method public hidebysig 
        instance void DoSomething (
            !T 'value'
        ) cil managed 
    {
        // Method begins at RVA 0x2094
        // Code size 16 (0x10)
        .maxstack 1
        .locals init (
            [0] string str
        )

        IL_0000: nop
        IL_0001: ldarga.s 'value'
        IL_0003: constrained. !T
        IL_0009: callvirt instance string [mscorlib]System.Object::ToString()
        IL_000e: stloc.0
        IL_000f: ret
    } // end of method CustomStack`1::DoSomething

    .method public hidebysig specialname rtspecialname 
        instance void .ctor () cil managed 
    {
        // Method begins at RVA 0x20b0
        // Code size 7 (0x7)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: call instance void [mscorlib]System.Object::.ctor()
        IL_0006: ret
    } // end of method CustomStack`1::.ctor

} // end of class ConsoleApplication16.CustomStack`1

Z IL widać jeszcze raz, że typy generyczne to nie sztuczka kompilatora, ale rozpoznawana konstrukcja w CLR. Przejdźmy teraz do metody, która nas najbardziej interesuje:

// Code size 16 (0x10)
.maxstack 1
.locals init (
    [0] string str
)

IL_0000: nop
IL_0001: ldarga.s 'value'
IL_0003: constrained. !T
IL_0009: callvirt instance string [mscorlib]System.Object::ToString()
IL_000e: stloc.0
IL_000f: ret

IL_0003 to contrained. !T.  Gdy callvirt wywołuje jakaś metodę, i wcześniej mamy contrained to zostanie boxing wykonany jeśli mamy do czynienia z value type. Innymi słowy, dla typów referencyjnych nic nie zmieni się (nie ma takiej potrzeby) a dla value najpierw wartość zostanie poddana boxingu.  Oczywiście mowa tylko o metodach wirtualnych. Jeśli wykonujemy metodę na typu value, która jest w nim zaimplementowana, wtedy nie ma takiej potrzeby.

Typy generyczne w .NET–implementacja wewnętrzna

Posted September 16th, 2014 by Piotr Zieliński
Categories: C#, Patterns & Practices

Typy generyczne w .NET to nie żadna imitacja, a prawdziwa implementacja (w przeciwieństwie do Java), zoptymalizowana pod wieloma kątami.

Typy referencyjne są zaimplementowane w inny sposób niż value types. Załóżmy, że mamy następujący kod w c#:

class Program
{
   static void Main(string[] args)
   {
       var s1 = new CustomStack<int>();
       var s2 = new CustomStack<double>();
   }
}

class CustomStack<T>
{
   public T Value { get; set; }
   public object OtherProperty { get; set; }
   
   public string AnotherProperty { get; set; }
}

W przypadku value type zostaną wygenerowane dwie różne specjalizacje dla CustomStack. Każda próba stworzenia instancji CustomStack z innym typem, spowoduje utworzenie nowej specjalizacji. Sprawa wygląda jednak inaczej dla typów referencyjnych. Rozszerzmy przykład o:

var s1 = new CustomStack<int>();
var s2 = new CustomStack<double>();

var s3 = new CustomStack<Person>();
var s4 = new CustomStack<Customer>();

Specjalizacje dla Person i Customer będą dzielić ze sobą layout. Nie ważne jak wiele stworzymy specjalizacji, zawsze będą współdzielić ten sam layout. W .NET nie grozi nam eksplozja typów, która miałaby miejsce, gdy nie powyższa optymalizacja.

Sprawdźmy jednak to sami za pomocą debuggera windbg.exe. Zaczynamy standardowo od załadowania SOS:

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

Następnie przyjrzyjmy się aktualnie załadowanym typom za pomocą:

!dumpheap -stat

Wynik:

image

Te co nas interesują  to:

003f3d08        1           20 ConsoleApplication16.CustomStack`1[[ConsoleApplication16.Customer, ConsoleApplication16]]
003f3c4c        1           20 ConsoleApplication16.CustomStack`1[[ConsoleApplication16.Person, ConsoleApplication16]]
003f399c        1           20 ConsoleApplication16.CustomStack`1[[System.Int32, mscorlib]]
003f3a80        1           24 ConsoleApplication16.CustomStack`1[[System.Double, mscorlib]]

Najpierw udowodnimy, że typy value nie współdzielą layoutu. Za pomocą powyższych adresów możemy wylistować method table używając następujących poleceń:

!dumpmt 003f3a80        
!dumpmt 003f399c               

Wynik:

// specjalizacja dla int
0:000> !dumpmt 003f399c        
EEClass:         003f1580
Module:          003f2ed4
Name:            ConsoleApplication16.CustomStack`1[[System.Int32, mscorlib]]
mdToken:         02000005
File:            C:\Users\Piotr\Documents\Visual Studio 2013\Projects\ConsoleApplication16\ConsoleApplication16\bin\Debug\ConsoleApplication16.exe
BaseSize:        0x14
ComponentSize:   0x0
Slots in VTable: 11
Number of IFaces in IFaceMap: 0

// specjalizacja dla double
0:000> !dumpmt 003f3a80        
EEClass:         003f15e0
Module:          003f2ed4
Name:            ConsoleApplication16.CustomStack`1[[System.Double, mscorlib]]
mdToken:         02000005
File:            C:\Users\Piotr\Documents\Visual Studio 2013\Projects\ConsoleApplication16\ConsoleApplication16\bin\Debug\ConsoleApplication16.exe
BaseSize:        0x18
ComponentSize:   0x0
Slots in VTable: 11
Number of IFaces in IFaceMap: 0

To co rzuca się w oczy to adres EEClass – jest inny dla int niż dla double. W przyszłości chcę napisać post o internalach typów, ale na razie wystarczy wiedzieć, że EEClass opisuje listę metod (layout) dla danego typu. Przyjrzyjmy się zatem EEClass dla specjalizacji double.

Polecenie:

!dumpclass 003f15e0

Wynik:

Class Name:      ConsoleApplication16.CustomStack`1[[System.Double, mscorlib]]
mdToken:         02000005
File:            C:\Users\Piotr\Documents\Visual Studio 2013\Projects\ConsoleApplication16\ConsoleApplication16\bin\Debug\ConsoleApplication16.exe
Parent Class:    73713f70
Module:          003f2ed4
Method Table:    003f3a80
Vtable Slots:    4
Total Method Slots:  b
Class Attributes:    100000  
Transparency:        Critical
NumInstanceFields:   3
NumStaticFields:     0
      MT    Field   Offset                 Type VT     Attr    Value Name
73b1b454  4000001        4        System.Double  1 instance           <Value>k__BackingField
73b22554  4000002        c        System.Object  0 instance           <OtherProperty>k__BackingField
73b221b4  4000003       10        System.String  0 instance           <AnotherProperty>k__BackingField

Wyraźnie widzimy, że layout zawiera pole double:

73b1b454  4000001        4        System.Double  1 instance           <Value>k__BackingField

EEClass dla specjalizacji integer, będzie zawierało analogiczny layout ale z innym typem dla Value:

      MT    Field   Offset                 Type VT     Attr    Value Name
73b23b04  4000001        c         System.Int32  1 instance           <Value>k__BackingField
73b22554  4000002        4        System.Object  0 instance           <OtherProperty>k__BackingField
73b221b4  4000003        8        System.String  0 instance           <AnotherProperty>k__BackingField

Powróćmy teraz do typów referencyjnych. Analogicznie za pomocą !dumpmt wyświetlamy method table dla nich:

// specjalizacja Customer
0:000> !dumpmt 003f3d08        
EEClass:         003f1694
Module:          003f2ed4
Name:            ConsoleApplication16.CustomStack`1[[ConsoleApplication16.Customer, ConsoleApplication16]]
mdToken:         02000005
File:            C:\Users\Piotr\Documents\Visual Studio 2013\Projects\ConsoleApplication16\ConsoleApplication16\bin\Debug\ConsoleApplication16.exe
BaseSize:        0x14
ComponentSize:   0x0
Slots in VTable: 11
Number of IFaces in IFaceMap: 0
// specjalizacja Person
0:000> !dumpmt 003f3c4c        
EEClass:         003f1694
Module:          003f2ed4
Name:            ConsoleApplication16.CustomStack`1[[ConsoleApplication16.Person, ConsoleApplication16]]
mdToken:         02000005
File:            C:\Users\Piotr\Documents\Visual Studio 2013\Projects\ConsoleApplication16\ConsoleApplication16\bin\Debug\ConsoleApplication16.exe
BaseSize:        0x14
ComponentSize:   0x0
Slots in VTable: 11
Number of IFaces in IFaceMap: 0

Od razu widać, że EEClass jest taki sam i znajduje się pod adresem 003f1694. Poleceniem !dumpclass 003f1694 wyświetlamy jej zawartość:

      MT    Field   Offset                 Type VT     Attr    Value Name
73b24d7c  4000001        4       System.__Canon  0 instance           <Value>k__BackingField
73b22554  4000002        8        System.Object  0 instance           <OtherProperty>k__BackingField
73b221b4  4000003        c        System.String  0 instance           <AnotherProperty>k__BackingField

Co widzimy? Przede wszystkim System.__Canon jako typ dla Value. Innymi słowy, dla typów referencyjnych jest używany ten sam layout i zamiast konkretnego typu mamy System.__Canon.

To jeszcze nie koniec- w kolejnym wpisie zobaczymy kilka ciekawostek z wywoływania metod w generics…

SpecFlow: konwersja argumentów

Posted September 13th, 2014 by Piotr Zieliński
Categories: Testy

O SpecFlow pisałem już kilkakrotnie. Zwykłe w pliku ze scenariuszami, podajemy argumenty. Na przykład:

Scenario: Add two numbers
    Given I have entered 50 into the calculator
    And I have entered 70 into the calculator
    When I press add
    Then the result should be 120 on the screen

Następnie w pliku c# możemy użyć regex:

[Given(@"I have entered (.*) into the calculator")]
public void GivenIHaveEnteredIntoTheCalculator(int p0)
{
  ScenarioContext.Current.Pending();
}

Czasami jednak, mamy bardziej skomplikowane wyrażenia i za każdym razem regex po prostu byłby niewygodny. Na szczęście możemy napisać własny konwertery.

Załóżmy, że mamy scenariusz typu “Given it happened 50 days ago.”.  Chcemy liczbę 50 skonwertować do DateTime .AddDays(-50). SpecFlow umożliwia własne transformacje:

[Binding]
public class SpecFlowFeature1Steps
{
   [StepArgumentTransformation(@"(\d+) days ago")]
   public DateTime ToDateTime(int days)
   {
       return DateTime.Now.AddDays(-days);
   }
   [Given(@"it happened (.*)")]
   public void GivenItHappenedDaysAgo_(DateTime dateTime)
   {
       ScenarioContext.Current.Pending();
   }
}

Wystarczy metodę konwertującą oznaczyć atrybutem StepArgummentTransformation.

Czasami w pliku scenariusza umieszczamy tabele danych np.:

Given I entered data about the following employees:
    | FirstName | LastName  | Title             |
    | Piotr     | Zielinski | Software Engineer |
    | A         | B         | C                 |

Klasyczny sposób dostępu do takich danych wyglądałby następująco:

[Given(@"I entered data about the following employees:")]
public void GivenIEnteredDataAboutTheFollowingEmployees(Table table)
{
  foreach (TableRow row in table.Rows)
  {
      string firstName = row["FirstName"];
      string lastName = row["LastName"];
  }
}

Podejście niezbyt eleganckie – zwykle chcemy operować na prawdziwych obiektach. Prawdopodobnie gdzieś w systemie mamy następujący obiekt:

public class Employee
{
   public string FirstName { get; set; }
   public string LastName { get; set; }
   public string Title { get; set; }
}

Wtedy za pomocą CreateSet możemy:

[Given(@"I entered data about the following employees:")]
public void GivenIEnteredDataAboutTheFollowingEmployees(Table table)
{
  IEnumerable<Employee> employees = table.CreateSet<Employee>();
}

Konwersja jest dokonywana na podstawie nazw. Kolumna FirstName jest kojarzona z właściwością o takiej samej nazwie. Rozmiar liter jednak nie ma znaczenia i zarówno FIRSTNAME jak i firstname zostaną skojarzone z właściwością FirstName. Również nie musimy martwić się o spacje czyli “first name” w pliku ze scenariuszami zostanie prawidłowo zinterpretowane. Dobrze o tym wiedzieć ponieważ scenariusze powinny wyglądać jak normalne zdania po angielsku a nie kod.

W jednym z postów pisałem o podobnym framework’u, a mianowicie SpecsFor. Zawierał on m.in expectedObjects – bardzo przydatną bibliotekę w walidacji obiektów oraz kolekcji obiektów. Zamiast samemu porównywać właściwość po właściwości, wystarczyło, że wywołaliśmy jedną metodę. Analogiczną rzecz możemy zrobić z tabelami w SpecFlow:

var expectedEmployees=new Employee[2];
expectedEmployees[0] = new Employee() {FirstName = "Piotr"};

table.CompareToSet(expectedEmployees);

Powyższy kod wywoła następujący wyjątek:

An exception of type 'TechTalk.SpecFlow.Assist.ComparisonException' occurred in TechTalk.SpecFlow.dll but was not handled in user code

Additional information: 

The table and the set not match.

Istnieje również funkcja do porównywania pojedynczych instancji:

table.CompareToInstance(employee);

Code Review: Przekazywanie metody jako parametr

Posted September 10th, 2014 by Piotr Zieliński
Categories: C#, Patterns & Practices

Ostatnio znalazłem fajny przykład pokazujący jak można nieoczekiwanie pogorszyć wydajność aplikacji. Załóżmy, że mamy metodę, która jako parametr wejściowy przyjmuje funkcję:

private static int Find(Predicate<int> filter)
{
  // jakas logika - nie wazne...

  if (filter(0))
      return 1;
  else
      return -1;
}

Nie zwracajmy uwagi na logikę i bezsensowne wartości. Chodzi mi o to, że przekazujemy jakiś wskaźnik na metodę, tutaj konkretnie jest to Predicate<int> np.:

private static bool CustomFilter(int value)
{
  return true;
}

Następnie w pętli, chcemy wykonać serie przeszukiwań na różnych danych:

for (int i = 0; i < 10000000; i++)
{
    Find(CustomFilter);
}

Powyższy kod wygląda niewinnie, jednak zajrzyjmy do IL Asm:

IL_0000: ldc.i4.0
IL_0001: stloc.0
IL_0002: br.s IL_001a
// loop start (head: IL_001a)
    IL_0004: ldnull
    IL_0005: ldftn bool ConsoleApplication15.Program::CustomFilter(int32)
    IL_000b: newobj instance void class [mscorlib]System.Predicate`1<int32>::.ctor(object, native int)
    IL_0010: call int32 ConsoleApplication15.Program::Find(class [mscorlib]System.Predicate`1<int32>)
    IL_0015: pop
    IL_0016: ldloc.0
    IL_0017: ldc.i4.1
    IL_0018: add
    IL_0019: stloc.0

    IL_001a: ldloc.0
    IL_001b: ldc.i4 10000000
    IL_0020: blt.s IL_0004
// end loop

Jak widzimy, w każdej iteracji, tuż przed wywołaniem Find jest tworzona nowa delegata:

IL_000b: newobj instance void class [mscorlib]System.Predicate`1<int32>::.ctor(object, native int)
IL_0010: call int32 ConsoleApplication15.Program::Find(class [mscorlib]System.Predicate`1<int32>)

Poprawne rozwiązanie to:

Predicate<int> customFilter = CustomFilter;

for (int i = 0; i < 10000000; i++)
{
 Find(customFilter);
}

IL:

IL_0000: ldnull
IL_0001: ldftn bool ConsoleApplication15.Program::CustomFilter(int32)
IL_0007: newobj instance void class [mscorlib]System.Predicate`1<int32>::.ctor(object, native int)
IL_000c: stloc.0
IL_000d: ldc.i4.0
IL_000e: stloc.1
IL_000f: br.s IL_001c
// loop start (head: IL_001c)
    IL_0011: ldloc.0
    IL_0012: call int32 ConsoleApplication15.Program::Find(class [mscorlib]System.Predicate`1<int32>)
    IL_0017: pop
    IL_0018: ldloc.1
    IL_0019: ldc.i4.1
    IL_001a: add
    IL_001b: stloc.1

    IL_001c: ldloc.1
    IL_001d: ldc.i4 10000000
    IL_0022: blt.s IL_0011
// end loop

W tym przypadku, nowy obiekt jest tworzony tylko raz – przed pętlą. Napiszmy również prosty benchmark:

class Program
{
   private static void Main(string[] args)
   {
       Stopwatch stopwatch = Stopwatch.StartNew();
       TestGoodSolution();
       Console.WriteLine(stopwatch.ElapsedTicks);

       stopwatch = Stopwatch.StartNew();
       TestBadSolution();
       Console.WriteLine(stopwatch.ElapsedTicks);
   }

   private static void TestBadSolution()
   {
       for (int i = 0; i < 10000000; i++)
       {
           Find(CustomFilter);
       }
   }

   private static void TestGoodSolution()
   {
       Predicate<int> customFilter = CustomFilter;

       for (int i = 0; i < 10000000; i++)
       {
           Find(customFilter);
       }
   }

   private static bool CustomFilter(int value)
   {
       return true;
   }

   private static int Find(Predicate<int> filter)
   {
       // jakas logika - nie wazne...

       if (filter(0))
           return 1;
       else
           return -1;
   }
}

Wynik:

image

Różnica spora a w rzeczywistości jest jeszcze większa. Pamiętajmy, że powyższy benchmark nie bierze pod uwagi Garbage Collector. Jeśli pętla jest wykonywana kilka tysięcy razey to również taka liczba obiektów zostanie zainicjalizowana i potem zwolniona przez GC, co może być bardzo kłopotliwe (promocja do następnej generacji itp.).

IL Assembly: IF vs. Switch, wydajność

Posted September 7th, 2014 by Piotr Zieliński
Categories: C#, Patterns & Practices

Dzisiaj kolejny temat, który bardzo często budzi wątpliwości. Część programistów preferuje zawsze IF, część z kolei switch. Niektórzy argumentują wybór wydajnością.

Zawsze powinniśmy kierować się czytelnością konkretnego kodu. W poście pokażę jak wygląda sprawa wydajności poprzez oczywiście IL Assembly. Sprawa jest dość skomplikowana i wiele zależy od konkretnych wartości jakie mamy w switch.

Zacznijmy od porównania prostego IF\Switch operującego na liczbach całkowitych:

static string TestSwitch(int value)
{
  switch (value)
  {
      case 0:
          return "zero";
      case 1:
          return "one";
      case 2:
          return "two";
      default:
          return "unknown";
  }
}
static string TestIf(int value)
{
  if (value == 0)
  {
      return "zero";
  }
  else if (value == 1)
  {
      return "one";
  }
  else if (value == 2)
  {
      return "two";
  }
  else
  {
      return "unknown";
  }
}

Obie metody robią dokładnie to samo – w zależności od przekazanego interger’a zwrócą konkretny string. Spróbujmy napisać prosty benchmark:

const int n = 10000000;

Stopwatch stopwatch1 = Stopwatch.StartNew();
for (int i = 0; i < n; i++)
    TestSwitch(i);

Console.WriteLine(stopwatch1.ElapsedTicks);

Stopwatch stopwatch = Stopwatch.StartNew();
for (int i = 0; i < n; i++)
    TestIf(i);

Console.WriteLine(stopwatch.ElapsedTicks);

Wynik będzie następujący:

image

Oznacza to, że switch jest trochę szybszy niż IF. Różnica jest na tyle jednak spora, że wypada zadać pytanie dlaczego? IL posiada różny zestaw instrukcji dla switch i IF. IF jest uzyskiwany za pomocą instrukcji skoków o których pisałem już wcześniej (m.in. BR).

Zajrzyjmy najpierw do IL asm dla IF:

IL_0000: ldarg.0
IL_0001: brtrue.s IL_0009

IL_0003: ldstr "zero"
IL_0008: ret

IL_0009: ldarg.0
IL_000a: ldc.i4.1
IL_000b: bne.un.s IL_0013

IL_000d: ldstr "one"
IL_0012: ret

IL_0013: ldarg.0
IL_0014: ldc.i4.2
IL_0015: bne.un.s IL_001d

IL_0017: ldstr "two"
IL_001c: ret

IL_001d: ldstr "unknown"
IL_0022: ret

Nie ma tutaj nic nowego – wszystko jest osiągnięte za pomocą warunkowych instrukcji skoków. Przyjrzyjmy się teraz switch:

IL_0000: ldarg.0
IL_0001: stloc.0
IL_0002: ldloc.0
IL_0003: switch (IL_0016, IL_001c, IL_0022)

IL_0014: br.s IL_0028

IL_0016: ldstr "zero"
IL_001b: ret

IL_001c: ldstr "one"
IL_0021: ret

IL_0022: ldstr "two"
IL_0027: ret

IL_0028: ldstr "unknown"
IL_002d: ret

Mamy tutaj specjalną instrukcję – switch. Jako parametry przyjmuje ona adresy konkretnych gałęzi. Jak dokonywane jest porównanie? To bardzo ciekawa sprawa…

W switch widzimy wyłącznie adresy gałęzi a nie warunki. switch bierze wartość ze stosu i jeśli jest ona równa zero wtedy wykonuje pierwszą gałąź, jeśli jest ona równa jeden to druga gałąź jest wykonywana itd.  Innymi słowy, wszystkie warunki zaczynają się od zero. Co się zatem stanie, gdy nasza logika ma inne warunki? Zmodyfikujmy przykład do:

switch (value)
{
 case 10:
     return "10";
 case 11:
     return "11";
 case 12:
     return "12";
 default:
     return "unknown";
}

IL:

IL_0000: ldarg.0
IL_0001: stloc.0
IL_0002: ldloc.0
IL_0003: ldc.i4.s 10
IL_0005: sub
IL_0006: switch (IL_0019, IL_001f, IL_0025)

IL_0017: br.s IL_002b

IL_0019: ldstr "10"
IL_001e: ret

IL_001f: ldstr "11"
IL_0024: ret

IL_0025: ldstr "12"
IL_002a: ret

IL_002b: ldstr "unknown"
IL_0030: ret

Sam switch wygląda tak samo. Różnica polega na tym, że odejmujemy od wartości liczbę 10, tak aby pierwsza gałąź zaczynała się wciąż od zero:

IL_0000: ldarg.0
IL_0001: stloc.0
IL_0002: ldloc.0
IL_0003: ldc.i4.s 10
IL_0005: sub

Innymi słowy zamiast porównywać wartość z 10,11,12, odejmujemy 10 i porównujemy z 0,1,2 – efekt końcowy dokładnie taki sam.

Kolejne pytanie, to co jeśli mamy wartości nieuporządkowane np. 10,12,14:

switch (value)
{
 case 10:
     return "10";
 case 12:
     return "12";
 case 14:
     return "14";
 default:
     return "unknown";
}

IL_0000: ldarg.0
IL_0001: stloc.0
IL_0002: ldloc.0
IL_0003: ldc.i4.s 10
IL_0005: sub
IL_0006: switch (IL_0021, IL_0033, IL_0027, IL_0033, IL_002d)

IL_001f: br.s IL_0033

IL_0021: ldstr "10"
IL_0026: ret

IL_0027: ldstr "12"
IL_002c: ret

IL_002d: ldstr "14"
IL_0032: ret

IL_0033: ldstr "unknown"
IL_0038: ret
} // end of method Progra

Znów odejmujemy 10, aby podstawą była wartość zero. Nasze warunki to 10,12,14 zatem  mamy przerwę między kolejnymi warunkami. Pamiętamy również, że instrukcja switch przyjmuje, że pierwszy warunek to “zero”, drugi “jeden” itd. Wygenerowany switch to:

IL_0006: switch (IL_0021, IL_0033, IL_0027, IL_0033, IL_002d)

Po prostu, kompilator wypełnił lukę adresem IL_0033, który wskazuje na klauzule default. Co jeśli mamy większe przerwy np.:

switch (value)
{
 case 100:
     return "100";
 case 200:
     return "200";
 case 300:
     return "300";
 default:
     return "unknown";
}

Zapełnianie switch adresami defualt byłoby mało wydajne – oznaczałoby to ponad 200 duplikatów. Zaglądając do IL przekonamy się, że kompilator wie co robi:


IL_0000: ldarg.0
IL_0001: stloc.0
IL_0002: ldloc.0
IL_0003: ldc.i4.s 100
IL_0005: beq.s IL_0019

IL_0007: ldloc.0
IL_0008: ldc.i4 200
IL_000d: beq.s IL_001f

IL_000f: ldloc.0
IL_0010: ldc.i4 300
IL_0015: beq.s IL_0025

IL_0017: br.s IL_002b

IL_0019: ldstr "100"
IL_001e: ret

IL_001f: ldstr "200"
IL_0024: ret

IL_0025: ldstr "300"
IL_002a: ret

IL_002b: ldstr "unknown"
IL_0030: ret

Switch został zamieniony do klasycznego IF’a. Nie będzie więc żadnej różnicy w wydajności. Dla wartości, które są dosyć daleko od siebie, użycie switch’a oznacza dokładnie to samo co IF.

Kompilator jest jednak bardzo sprytny. Załóżmy, że mamy następujący IF:

switch (value)
{
    case 100:
        return "100";
    case 101:
        return "101";
    case 102:
        return "102";

    case 300:
        return "300";
    case 301:
        return "301";
    case 302:
        return "302";

    case 400:
        return "400";
    case 401:
        return "401";
    case 402:
        return "402";
    default:
        return "unknown";
}

Wartości od siebie różnią się skrajnie. Możemy jednak dostrzec pewne klastry. Kompilator wykryje to i stworzy trzy klastry oparte na switch:

IL_0000: ldarg.0
IL_0001: stloc.0
IL_0002: ldloc.0
IL_0003: ldc.i4.s 100
IL_0005: sub
IL_0006: switch (IL_0049, IL_004f, IL_0055)

IL_0017: ldloc.0
IL_0018: ldc.i4 300
IL_001d: sub
IL_001e: switch (IL_005b, IL_0061, IL_0067)

IL_002f: ldloc.0
IL_0030: ldc.i4 400
IL_0035: sub
IL_0036: switch (IL_006d, IL_0073, IL_0079)

IL_0047: br.s IL_007f

IL_0049: ldstr "100"
IL_004e: ret

IL_004f: ldstr "200"
IL_0054: ret

IL_0055: ldstr "200"
IL_005a: ret

IL_005b: ldstr "300"
IL_0060: ret

IL_0061: ldstr "300"
IL_0066: ret

IL_0067: ldstr "300"
IL_006c: ret

IL_006d: ldstr "300"
IL_0072: ret

IL_0073: ldstr "300"
IL_0078: ret

IL_0079: ldstr "300"
IL_007e: ret

IL_007f: ldstr "unknown"
IL_0084: ret

Nie musimy wypełniać tutaj żadnej luki w switch, bo każdy klaster jest spójny.

Enumy na switch działają tak samo jak integer’y bo tak naprawdę niczym nie różnią się (enum to integer).

Przetwarzanie string’ów to jednak zupełnie inna historia. Przykład:

switch (text)
{
 case "1":
     return 1;
 case "2":
     return 2;
 case "3":
     return 3;

 default:
     return -1;
}

Wygenerowany IL:

IL_0000: ldarg.0
IL_0001: dup
IL_0002: stloc.0
IL_0003: brfalse.s IL_0034

IL_0005: ldloc.0
IL_0006: ldstr "1"
IL_000b: call bool [mscorlib]System.String::op_Equality(string, string)
IL_0010: brtrue.s IL_002e

IL_0012: ldloc.0
IL_0013: ldstr "2"
IL_0018: call bool [mscorlib]System.String::op_Equality(string, string)
IL_001d: brtrue.s IL_0030

IL_001f: ldloc.0
IL_0020: ldstr "3"
IL_0025: call bool [mscorlib]System.String::op_Equality(string, string)
IL_002a: brtrue.s IL_0032

IL_002c: br.s IL_0034

IL_002e: ldc.i4.1
IL_002f: ret

IL_0030: ldc.i4.2
IL_0031: ret

IL_0032: ldc.i4.3
IL_0033: ret

IL_0034: ldc.i4.m1
IL_0035: ret

Jak widzimy, kod niczym nie różni się od IF’ow – wydajność będzie taka sama. Switch dla napisów został skonwertowany do IF’ow. Sprawa jednak będzie wyglądać trochę inaczej, jeśli dołożymy kilka nowych warunków:

switch (text)
{
 case "1":
     return 1;
 case "2":
     return 2;
 case "3":
     return 3;
 case "4":
     return 4;
 case "5":
     return 5;
 case "6":
     return 6;
 case "7":
     return 7;
 case "8":
     return 8;

 default:
     return -1;
}

IL:

IL_0000: ldarg.0
IL_0001: dup
IL_0002: stloc.0
IL_0003: brfalse IL_00c7

IL_0008: volatile.
IL_000a: ldsfld class [mscorlib]System.Collections.Generic.Dictionary`2<string, int32> '<PrivateImplementationDetails>{F94F7878-6940-44E5-A06D-C84605A56EC1}'::'$$method0x6000002-1'
IL_000f: brtrue.s IL_007e

IL_0011: ldc.i4.8
IL_0012: newobj instance void class [mscorlib]System.Collections.Generic.Dictionary`2<string, int32>::.ctor(int32)
IL_0017: dup
IL_0018: ldstr "1"
IL_001d: ldc.i4.0
IL_001e: call instance void class [mscorlib]System.Collections.Generic.Dictionary`2<string, int32>::Add(!0, !1)
IL_0023: dup
IL_0024: ldstr "2"
IL_0029: ldc.i4.1
IL_002a: call instance void class [mscorlib]System.Collections.Generic.Dictionary`2<string, int32>::Add(!0, !1)
IL_002f: dup
IL_0030: ldstr "3"
IL_0035: ldc.i4.2
IL_0036: call instance void class [mscorlib]System.Collections.Generic.Dictionary`2<string, int32>::Add(!0, !1)
IL_003b: dup
IL_003c: ldstr "4"
IL_0041: ldc.i4.3
IL_0042: call instance void class [mscorlib]System.Collections.Generic.Dictionary`2<string, int32>::Add(!0, !1)
IL_0047: dup
IL_0048: ldstr "5"
IL_004d: ldc.i4.4
IL_004e: call instance void class [mscorlib]System.Collections.Generic.Dictionary`2<string, int32>::Add(!0, !1)
IL_0053: dup
IL_0054: ldstr "6"
IL_0059: ldc.i4.5
IL_005a: call instance void class [mscorlib]System.Collections.Generic.Dictionary`2<string, int32>::Add(!0, !1)
IL_005f: dup
IL_0060: ldstr "7"
IL_0065: ldc.i4.6
IL_0066: call instance void class [mscorlib]System.Collections.Generic.Dictionary`2<string, int32>::Add(!0, !1)
IL_006b: dup
IL_006c: ldstr "8"
IL_0071: ldc.i4.7
IL_0072: call instance void class [mscorlib]System.Collections.Generic.Dictionary`2<string, int32>::Add(!0, !1)
IL_0077: volatile.
IL_0079: stsfld class [mscorlib]System.Collections.Generic.Dictionary`2<string, int32> '<PrivateImplementationDetails>{F94F7878-6940-44E5-A06D-C84605A56EC1}'::'$$method0x6000002-1'

IL_007e: volatile.
IL_0080: ldsfld class [mscorlib]System.Collections.Generic.Dictionary`2<string, int32> '<PrivateImplementationDetails>{F94F7878-6940-44E5-A06D-C84605A56EC1}'::'$$method0x6000002-1'
IL_0085: ldloc.0
IL_0086: ldloca.s CS$0$0001
IL_0088: call instance bool class [mscorlib]System.Collections.Generic.Dictionary`2<string, int32>::TryGetValue(!0, !1&)
IL_008d: brfalse.s IL_00c7

IL_008f: ldloc.1
IL_0090: switch (IL_00b7, IL_00b9, IL_00bb, IL_00bd, IL_00bf, IL_00c1, IL_00c3, IL_00c5)

IL_00b5: br.s IL_00c7

IL_00b7: ldc.i4.1
IL_00b8: ret

IL_00b9: ldc.i4.2
IL_00ba: ret

IL_00bb: ldc.i4.3
IL_00bc: ret

IL_00bd: ldc.i4.4
IL_00be: ret

IL_00bf: ldc.i4.5
IL_00c0: ret

IL_00c1: ldc.i4.6
IL_00c2: ret

IL_00c3: ldc.i4.7
IL_00c4: ret

IL_00c5: ldc.i4.8
IL_00c6: ret

IL_00c7: ldc.i4.m1
IL_00c8: ret

Jak widzimy, kod opiera swoje działanie na słowniku danych. Kluczem jest oczywiście warunek, a wartością indeks gałęzi. Następnie wywołujemy TryGetValue na słowniku, przekazując parametr wejściowy (string). Uzyskamy w ten sposób numer gałęzi. Potem wystarczy, że wywołamy switch (indeks gałęzi zaczyna się od zero) i jak w poprzednich przykładach, zwrócimy odpowiednie wartości:

IL_0090: switch (IL_00b7, IL_00ba, IL_00bc, IL_00be, IL_00c0, IL_00c2, IL_00c4, IL_00c6)

IL_00b5: br.s IL_00c8

IL_00b7: ldc.i4.s 1
IL_00b9: ret

IL_00ba: ldc.i4.2
IL_00bb: ret

IL_00bc: ldc.i4.3
IL_00bd: ret

IL_00be: ldc.i4.4
IL_00bf: ret

IL_00c0: ldc.i4.5
IL_00c1: ret

IL_00c2: ldc.i4.6
IL_00c3: ret

IL_00c4: ldc.i4.7
IL_00c5: ret

IL_00c6: ldc.i4.8
IL_00c7: ret

IL_00c8: ldc.i4.m1
IL_00c9: ret

Powyższy przykład można byłoby zoptymalizować ponieważ indeksy gałęzi pokrywają się z wartościami jakie chcemy zwrócić. W praktyce jednak te dwie rzeczy są kompletnie od siebie niezależne.

Na koniec pozostaje napisać kolejny benchmark, porównujący IF dla napisów oraz SWITCH, który wygeneruje wewnętrzny słownik:

class Program
{
   private static void Main(string[] args)
   {
       const int n = 10000000;

       Stopwatch stopwatch1 = Stopwatch.StartNew();
       for (int i = 0; i < n; i++)
           TestSwitch(i.ToString());
       Console.WriteLine(stopwatch1.ElapsedTicks);


       Stopwatch stopwatch = Stopwatch.StartNew();
       for (int i = 0; i < n; i++)
           TestIf(i.ToString());
       Console.WriteLine(stopwatch.ElapsedTicks);
   }
   static int TestSwitch(string text)
   {
       switch (text)
       {
           case "1":
               return 1;
           case "2":
               return 2;
           case "3":
               return 3;
           case "4":
               return 4;
           case "5":
               return 5;
           case "6":
               return 6;
           case "7":
               return 7;
           case "8":
               return 8;

           default:
               return -1;
       }
   }
   static int TestIf(string text)
   {
       if (text == "1")
       {
           return 1;
       }
       else if (text == "2")
       {
           return 2;
       }
       else if (text == "3")
       {
           return 3;
       }
       else if (text == "4")
       {
           return 4;
       }
       else if (text == "5")
       {
           return 5;
       }
       else if (text == "6")
       {
           return 6;
       }
       else if (text == "7")
       {
           return 7;
       }
       else if (text == "8")
       {
           return 8;
       }
       else
       {
           return -1;
       }
   }
}

Wynik:

image

Jak widać nie ma większej różnicy dla przedstawionych warunków. Jeśli mamy ich dużo więcej, wtedy i tak nie chcemy przecież korzystać z IF lub SWITCH. Przewaga SWITCH ma miejsce wyłącznie w bardzo specyficznych warunkach, w których wartości są bardzo blisko siebie (nie ma przerw). Klasycznym przykładem są ENUM’y, które zwykle zawierają sekwencyjne wartości.

IL assembly: foreach vs. for, wydajność

Posted September 4th, 2014 by Piotr Zieliński
Categories: C#, Patterns & Practices

Dziś kolejny wpis na temat mikro-optymalizacji. Oczywiście dla większości aplikacji biznesowych taka różnica w wydajności nie ma kluczowego znaczenia. Myślę jednak, że jest to ciekawe z punktu widzenia IL i jak naprawdę działa język c#. Jeśli ktoś z kolei piszę np. grę albo aplikację czasu rzeczywistego, wtedy ma to już znaczenie, co robimy w każdej sekundzie.

Zacznijmy od razu od wniosku: foreach w niektórych przypadkach jest znacząco wolniejszy od klasycznego for. Nie powinno to dziwić – w końcu iterator dodaję kolejną warstwę abstrakcji.

Spójrzmy na poniższy program, który posłuży nam jako dowód:

internal class Program
{
   private static void Main(string[] args)
   {
       int[] data = Enumerable.Range(0, 1000000).ToArray();

       Stopwatch stopWatch1 = Stopwatch.StartNew();
       TestForeachWithArray(data);
       Console.WriteLine(stopWatch1.ElapsedTicks);

       Stopwatch stopWatch2 = Stopwatch.StartNew();
       TestForeachWithEnumerable(data);
       Console.WriteLine(stopWatch2.ElapsedTicks);

       Stopwatch stopWatch3 = Stopwatch.StartNew();
       TestForWithArray(data);
       Console.WriteLine(stopWatch3.ElapsedTicks);
   }

   private static void TestForeachWithArray(int[] data)
   {
       foreach (int item in data)
       {

       }
   }

   private static void TestForeachWithEnumerable(IEnumerable<int> data)
   {
       foreach (int item in data)
       {

       }
   }

   private static void TestForWithArray(int[] data)
   {
       for (int i = 0; i < data.Length; i++)
       {
           int item = data[i];
       }
   }
}

Wynik:

image

Program przedstawia 3 testy. Pierwszy korzysta z foreach, ale operuje na zwykłej tablicy danych. Drugi również jest oparty na foreach, ale przechodzi przez typ enumerable. Ostatni test korzysta ze zwykłej pętli for i tablicy danych. Wynik foreach+tablica jest bardzo podobny do for+tablica. Połączenie jednak foreach i enumerable jest dużo wolniejsze. Dzieje się to mimo faktu, że enumerable wskazuje tak naprawdę na tablicę więc teoretycznie nie powinno mieć to znaczenia.

Pozostaje wyjaśnić dlaczego wyłącznie foreach+enumerable jest taki wolny. Ostatnie wpisy były o IL assembly, stąd nie powinno budzić zaskoczenia, że ILSpy czy Reflector będzie pierwszym narzędziem jakim posłużymy się.

IL dla TestForeachWithArray:

IL_0000: ldarg.0
IL_0001: stloc.0
IL_0002: ldc.i4.0
IL_0003: stloc.1
IL_0004: br.s IL_000e
// loop start (head: IL_000e)
    IL_0006: ldloc.0
    IL_0007: ldloc.1
    IL_0008: ldelem.i4
    IL_0009: pop
    IL_000a: ldloc.1
    IL_000b: ldc.i4.1
    IL_000c: add
    IL_000d: stloc.1

    IL_000e: ldloc.1
    IL_000f: ldloc.0
    IL_0010: ldlen
    IL_0011: conv.i4
    IL_0012: blt.s IL_0006
// end loop

TestForWithArray:

IL_0000: ldc.i4.0
IL_0001: stloc.0
IL_0002: br.s IL_000c
// loop start (head: IL_000c)
    IL_0004: ldarg.0
    IL_0005: ldloc.0
    IL_0006: ldelem.i4
    IL_0007: pop
    IL_0008: ldloc.0
    IL_0009: ldc.i4.1
    IL_000a: add
    IL_000b: stloc.0

    IL_000c: ldloc.0
    IL_000d: ldarg.0
    IL_000e: ldlen
    IL_000f: conv.i4
    IL_0010: blt.s IL_0004

Zanim przejdziemy do TestForeachWithEnumerable, przeanalizujmy powyższe dwa fragmenty kodu. Wyglądają one praktycznie tak samo. Obydwa korzystają z instrukcji skoku blt. Tak naprawdę kompilacja (pierwsza faza dokonywana przez VS) zamieni foreach na zwykł for, jeśli operujemy na tablicy danych. Niestety taka konwersja jest niemożliwa, gdy korzystamy z enumerable, nawet jak wskazuje on na tablicę. IL dla TestForeachWithEnumerable wygląda bardzo skomplikowanie:

IL_0000: ldarg.0
IL_0001: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> class [mscorlib]System.Collections.Generic.IEnumerable`1<int32>::GetEnumerator()
IL_0006: stloc.0
.try
{
    IL_0007: br.s IL_0010
    // loop start (head: IL_0010)
        IL_0009: ldloc.0
        IL_000a: callvirt instance !0 class [mscorlib]System.Collections.Generic.IEnumerator`1<int32>::get_Current()
        IL_000f: pop

        IL_0010: ldloc.0
        IL_0011: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
        IL_0016: brtrue.s IL_0009
    // end loop

    IL_0018: leave.s IL_0024
} // end .try
finally
{
    IL_001a: ldloc.0
    IL_001b: brfalse.s IL_0023

    IL_001d: ldloc.0
    IL_001e: callvirt instance void [mscorlib]System.IDisposable::Dispose()

    IL_0023: endfinally
} // end handler

Od razu widać, że foreach dodaje bardzo dużo komplikacji. Można zauważyć m.in. try-catch-finally oraz drogą instrukcję callvirt – wywołanie metody wirtualnej, co spowoduje przeszukanie vtable.

W aplikacjach biznesowych ważniejsza jest jednak łatwość w utrzymaniu, stąd zwykle posiadamy wiele warstw w systemie, aby operować na abstrakcji. W takich sytuacjach ważniejsze dla nas jest, aby np. odseparować sposób w jaki przeglądamy dane od nich samych. Nie są to podejścia najbardziej wydajne, ale nie stanowią problemu dla wspomnianych aplikacji. Inne podejście akurat mają programiści np. sterowników kart graficznych czy gier komputerowych. Tam po prostu są inne wyzwania i priorytety.

Zewnętrzne aliasy – extern alias

Posted August 31st, 2014 by Piotr Zieliński
Categories: C#, Patterns & Practices

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

Posted August 28th, 2014 by Piotr Zieliński
Categories: C#, Patterns & Practices

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ę

Posted August 20th, 2014 by Piotr Zieliński
Categories: Patterns & Practices, Visual Studio

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

Posted August 17th, 2014 by Piotr Zieliński
Categories: C#, Patterns & Practices

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.