Wydajność: Porównanie operatorów as, is oraz rzutowania bezpośredniego (prefiksowego)

Zgodnie z obietnicą dziś napiszę trochę więcej o rzutowaniu, wydajności oraz dobrych praktykach. Muszę przyznać, że w jednej kwestii miałem nieprawdziwe informacje (znalezione gdzieś na forum) których byłem pewien ponieważ napisałem prosty program sprawdzający wydajność – jak na końcu pokażę popełniłem błąd podczas mierzenia wydajności spowodowany kompilacją JIT.

Na początku trochę przypomnienia:

1. Rzutowanie prefiksowe – najpopularniejszy typ znany np. z CPP.

int value = (int)boxedValue;

Można wykorzystać zarówno do rzutowania typów VALUE jak i referencyjnych. Bierze również pod uwagę własne operatory rzutowania explicit. Gdy typ się nie zgadza zostaje wyrzucony wyjątek. Zwykle rozwiązanie wolniejsze niż operator as.

2. Operator rzutowania as. Często uważany za szybsze rozwiązanie co jednak nie zawsze jest prawdą. Gdy nie można dokonać rzutowania wtedy zwracana jest wartość NULL. Niestety nie można wykorzystać dla typów prostych (value) oraz własnych operatorów rzutowania (explicit).

FileInfo = obj as FileInfo;

3. Operator is. Sprawdza czy obiekt jest danego typu.

if(obj is FileInfo)
{
    //...
}

Przejdźmy do pomiaru wydajności. Dokonamy 20 pomiarów, w którym pojedynczy pomiar to kilkadziesiąt operacji rzutowania. Zacznijmy od rozwiązania prefiksowego. Kod:

object genericObject = new A();
const int iterationsNumber = 10000;
const int samplesNumber = 20;


Stopwatch stopwatch = new Stopwatch();
long[,] samples = new long[samplesNumber,2];

for (int i = 0; i < samplesNumber; i++)
{
 stopwatch.Restart();

 int castsNumber = iterationsNumber*i+1;

 for (int j = 0; j < castsNumber; j++)
 {
     A castedObject = genericObject as A;
     if (castedObject != null)
     {

     }
 }

 stopwatch.Stop();
 samples[i, 0] = castsNumber;
 samples[i, 1] = stopwatch.ElapsedTicks;
}

Wartości:

Liczba rzutowań Ticks

1

360

10001

131

20001

258

30001

385

40001

514

50001

641

60001

779

70001

895

80001

1022

90001

1140

100001

1305

110001

1391

120001

1598

130001

1650

140001

1777

150001

1937

160001

2041

170001

2170

180001

2297

190001

2424

Wykres:

image

Na początku wspomniałem, że porównując wydajność as z prefiksowym popełniłem błąd. Spoglądając na tabelę widać, że pierwszy pomiar pokazuje, że rzutowanie 1 wartości było wolniejsze niż następne 10 000. Wcześniej po prostu najpierw zmierzyłem wydajność as, potem prefiksowego i dlatego te drugie wyszło mi bardzo szybkie. Jest to jednak spowodowane kompilacją JIT oraz wywołaniem metod StopWatcher’a – gdy pierwszy raz wywołujemy jakąkolwiek metodę, zwykle zabiera to więcej czasu niż następne (kompilacja).

Widzimy, ze koszt jest liniowy – to dobrze. Sprawdźmy teraz np. za pomocą .NET Reflector jak wygląda kod IL. Kod jest długi więc wklejam tylko najważniejszą linie wykonującą as:

L_0050: isinst WindowsFormsApplication2.A

Instrukcja isinst dokonuję rzutowania. Warto zaznaczyć, że isinst jest uważana za rozwiązanie wydajne,  z tym, że nie sprawdza rzutowań  zdefiniowanych przez użytkownika (explicit) – może dlatego jest szybsze…Jeśli mamy jakieś własne operatory rzutowania to należy skorzystać z rozwiązania prefiksowego.

Przejdźmy do pomiaru rzutowania klasycznego i operatora is. Kod:

object genericObject = new A();
const int iterationsNumber = 10000;
const int samplesNumber = 20;


Stopwatch stopwatch = new Stopwatch();
long[,] samples = new long[samplesNumber,2];

for (int i = 0; i < samplesNumber; i++)
{
 stopwatch.Restart();

 int castsNumber = iterationsNumber*i+1;

 for (int j = 0; j < castsNumber; j++)
 {
     if (genericObject is A)
     {
         A castedObject = (A) genericObject;
     }
 }

 stopwatch.Stop();
 samples[i, 0] = castsNumber;
 samples[i, 1] = stopwatch.ElapsedTicks;
}

Wynik:

Liczba rzutowań Ticks

1

357

10001

160

20001

316

30001

474

40001

628

50001

784

60001

942

70001

1097

80001

1254

90001

1410

100001

1571

110001

1722

120001

1880

130001

2373

140001

2196

150001

2349

160001

2535

170001

3016

180001

2838

190001

2980

Wykres:

image

Program został zrestartowany i można zauważyć, że rzutowanie prefiksowe jest nieco wolniejsze. Pierwszy wynik został również przekłamany z powodu JIT. Zajrzyjmy teraz jednak do .NET Reflector. Najpierw jak wygląda c# po wstępnej kompilacji:

Label_004E:
if ((((genericObject as A) > null) == 0) != null)
{
   goto Label_006B;
}
castedObject = (A) genericObject;

Zaraz, zaraz… Co tu robi operator as? W końcu użyliśmy is+prefix. Niestety is to tak na prawdę najpierw zrzutowanie za pomocą as a potem sprawdzenie czy jest on różny od NULL. Z tego względu taka konstrukcja jest wolna. Przejdźmy teraz do ID:

L_0050: isinst WindowsFormsApplication2.A
L_0055: ldnull 
L_0056: cgt.un 
L_0058: ldc.i4.0 
L_0059: ceq 
L_005b: stloc.s CS$4$0000
L_005d: ldloc.s CS$4$0000
L_005f: brtrue.s L_006b
L_0061: nop 
L_0062: ldloc.0 
L_0063: castclass WindowsFormsApplication2.A

Widzimy instrukcję isinst a następnie castclass. isinst powinna być już nam znana – jest to operator as. Z kolei castclass to rzutowanie bezpośrednie (prefiksowe).

Porównanie wydajności:

image

Jak widać, pomimo, że w IL jest niepotrzebne rzutowanie, wyniki są zbliżone jednak z lekką przewagą na as zamiast na is+prefix.

A co w przypadku gdy mamy tylko as albo rzutowanie prefiksowe – bez zbędnego operatora is? Na zdrowy rozsądek is powinien być szybszy ponieważ skoro as+prefix jest nieco wolniejszy tylko od samego as tzn., że rzutowanie prefiksowe nie narzuca zbyt wiele. Sprawdziłem i zależy to generalnie od hierarchii klas. Ponadto nawet pojedynczy IF potrafi zając sporą cześć czasu w porównaniu do rzutowania. Sprawa jest więc skomplikowana ponieważ wydajność zależy od JIT-compile, hierarchii klas itp. Przeglądałem inne próby oszacowania wydajności i padają różne wyniki: czasami prefix szybszy a czasami as. Spotkałem się również z opinią, że prefiksowe rzutowanie było dużo wolniejsze w .NET 1.1 jednak od następnych wersji różnica między as a prefix zanikła a nawet prefiksowe może być nieco szybsze. Widać, że wydajność jest trudna do oszacowania. Spróbujmy jednak wyciągnąć kilka wniosków z tego postu:

1. Użycie operatora is z rzutowaniem prefiksowym jest trochę wolniejsze bo musimy wykonać podwójne rzutowanie – lepiej wykorzystać po prostu as.

2. Wydajność rzutowania prefiksowego a as jest trudna do oszacowania ponieważ zależy od wielu czynników. Różnica jest również niewielka wiec uważam, że nie powinniśmy się tym martwić.

3. Używaj as tylko gdy istnieje możliwość błędnego rzutowania i przewidujesz to w aplikacji wprowadzając dodatkowy warunek. Jeśli spodziewasz się, że po zrzutowaniu powinieneś mieć zawszę poprawną wartość wtedy użyj rzutowania prefiksowego, które w razie niepowodzenia wyrzuci wyjątek. Operator as zwraca tylko NULL więc ewentualne problemy będą ciężkie w diagnozie – dużo więcej informacji mówi wyjątek InvalidCast (z opisem dokładnych obiektów) niż po prostu NULL Reference.

Innymi słowy, używaj rzutowania prefiksowego wyłącznie gdy nie przewidujesz sytuacji, że rzutowanie nie zakończy się sukcesem. Łapanie wyjątku jest dużo wolniejsze niż sprawdzanie wartości NULL.

Code review: rzutowanie

Co powiedzie na taki fragment kodu?

FileInfo fileInfo;
if (sender is FileInfo)
    fileInfo = sender as FileInfo;

Konstrukcja jest dość popularna i:

– oczywiście skompiluje się,

– jest bezpieczna na wartość NULL (tzn. nie wyrzuci wyjątku),

– jeśli sender jest innego typu niż FileInfo, kod nie wyrzuci wyjątku.

Co jest w końcu nie tak? Chodzi tutaj o good practice i nie wprowadzanie czytelnika kodu w błąd. Operator is sprawdza czy obiekt jest danego typu. Zatem w instrukcji IF wiemy już,  że sender MUSI być typu FileInfo. Z kolei słówko as próbuje zrzutować dany obiekt na określony typ. Jeśli takowe rzutowanie jest niemożliwe, wtedy po prostu zwracana jest wartość NULL. W przeciwieństwie do castowania typu  fileInfo = (FileInfo)sender, kod nie wyrzuci wyjątku a wartość NULL. Z tego wzglądu lepszym podejściem jest:

FileInfo fileInfo = sender as FileINfo;
if (fileInfo != null)
{
}

Ponadto użycie is oraz as powoduje podwójne rzutowanie – ale o tym w następnym poście…

Przydatne ikony w Visual Studio

Ostatnio byłem offline (wakacje) ale od dziś znów posty powinny pojawiać się regularnie. Na początek przydatna ciekawostka. Jeśli piszecie aplikacje desktop i potrzebujecie standardowych ikon dla np. toolbar, w folderze Common7 (Visual Studio, np. “C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7)  znajdziecie paczkę np. VS2010ImageLibrary.zip.

Znajduje się tam wiele ikon np. Save, Undo, Redo itp. Pamiętam, że paczka była już obecna tam od kilku wersji Visual Studio. Nie jest to zbyt popularna lokalizacja dlatego też postanowiłem o tym napisać.

Yield oraz yield-break

Słowo kluczowe yield jest dość często wykorzystywane w c#. W przeciwieństwie do poprzednich konstrukcji, które opisywałem (np. volatile), nie trudno znaleźć zastosowanie praktycznie w projekcie. Zacznijmy  może od razu od przykładu:

foreach (int value in GetNumbers(5,7))
{
 MessageBox.Show(value.ToString());
}
// ------------------
private IEnumerable<int> GetNumbers(int start,int end)
{
  for (int i = start; i < end; i++)
      yield return i;
}

Pamiętam, że jak pierwszy raz czytałem o yield, mylące dla mnie było słowo return – wydawało mi się, że funkcja zakończy swoje działanie i zwróci pojedynczy wynik. Operator yield  służy do zwracania wartości składowych IEnumerable. Powyższy przykład zwróci liczby od start do end. Należy jednak pamiętać, że wywołując funkcję GetNumbers(5,10) generowana jest za każdym razem tylko jedna wartość np:

foreach (int value in GetNumbers(5,10))
{
 break;
}

Pętla for w GetNumbers wykona tylko jedną iteracje (zwróci wartość 5) . Gdybyśmy zwracali wynik jako tablica, od razu wszystko musiałoby zostać wygenerowane. Ma to znaczenie dla operacji czasochłonnych, zagnieżdżonych pętli oraz sytuacji gdy istnieje prawdopodobieństwo przerwania pętli na podstawie jakiegoś warunku.

Z kolei yield-break przerywa generowanie IEnumerable i nie zwraca żadnego wyniku:

private IEnumerable<int> GetNumbers(int start,int end)
{
  for (int i = start; i < end; i++)
  {
      if(i>6)
          yield break;
      yield return i;
  }
}

Zamiast yield można oczywiście napisać własny Enumerator, zaimplementować MoveNext, Current itp. Jednak w wielu przypadkach konstrukcja yield jest znacznie prostsza. Warto rozważyć yield również zamiast tworzenia listy wyników i późniejszego zwrócenia wyników jako całość. W końcu jeśli użytkownik chce od razu wszystkie wyniki, może wywołać metodę ToArray na IEnumerable.

Operatory implicit oraz explicit

Rozważmy, dwie zupełnie bezsensowne klasy:

class TextInfo
{
   public string Text { get; set; }
}
class NumberInfo
{
   public int Number { get; set; }
}

Następnie gdzieś w kodzie próbujemy dokonać konwersji:

NumberInfo numberInfo = new NumberInfo {Number = 43};

TextInfo textInfo1 = numberInfo; // konwersja niejawna
TextInfo textInfo2 = (TextInfo)numberInfo; // konwersja jawna

Czy, którakolwiek przedstawiona konwersja zakończy się sukcesem?Oczywiście, że nie – skąd .NET ma wiedzieć jak należy zamienić jeden typ na drugi? Na szczęście .NET dostarcza operatory zarówno do konwersji niejawnej (pierwszy przykład, jak i jawnej (drugi przykład). Operator implicit służy do konwersji niejawnej:

internal class TextInfo
{
   public string Text { get; set; }

   public static implicit operator TextInfo(NumberInfo numberInfo)
   {
       return new TextInfo {Text = numberInfo.Number.ToString()};
   }
}

Naturalnie, operator konwersji niejawnej umożliwia również konwersję jawną, zatem oba powyższe przykłady zostaną skompilowane z sukcesem. “Słabszym”, operatorem jest explicit – wyłącznie konwersja jawna:

internal class TextInfo
{
   public string Text { get; set; }

   public static explicit operator TextInfo(NumberInfo numberInfo)
   {
       return new TextInfo {Text = numberInfo.Number.ToString()};
   }
}

Osobiście nie miałem okazji zastosować w praktyce (w realnym projekcie) powyższych konstrukcji. Myślę jednak, że w bibliotekach matematycznych, algorytmach (choćby różne typy sieci neuronowych) ma to rację bytu. Szczególnie należy zastanowić się przed użyciem operatora implicit– może spowodować zamieszanie.

C#: słowo kluczowe volatile

W ostatnich kilku postach przedstawiłem “egzotyczne” słowa kluczowe w c#. Wiele z nich, myślę, nie było znanych nawet bardziej zaawansowanym programistom. Z pewnością nie są one niezbędne do pisania aplikacji. Często nawet nie jest wskazane aby z nich korzystać, chyba, że naprawdę dokładnie przeanalizowaliśmy sytuację. Słowo zaprezentowane w dzisiejszym poście również zalicza się do tego zbioruUśmiech.  Myślę, że volatile może być znane programistom C++ ale w świecie c# jest dużo mniej popularne. Jeśli piszecie aplikacje czysto wielozadaniowe wtedy volatile prawdopodobnie będzie należał do zbioru najważniejszych słów kluczowych. Dla programistów aplikacji biznesowych, które nie korzystają z zawansowanego modelu współbieżnego, volatile wpiszę się zdecydowanie tylko w rzadkie scenariusze.

Niestety, w oficjalnej dokumentacji MSDN brakuje dobrego opisu i przykładów. Zacznijmy od definicji. Volatile w c# mówi kompilatorowi aby nie dokonywał optymalizacji na danym polu. Przykład:

class SampleClass
{
    private volatile bool _flag;
}

Od tej pory _flag nie będzie zawsze optymalizowany przez kompilator. Zmiany można zaobserwować w trybie Release. W Debug zwykle nie ma bardziej skomplikowanych optymalizacji. Takie informacje pewnie nie są zbyt praktyczne i wypadałoby określić jakiego typu są to optymalizacje? I dlaczego czasami warto z nich zrezygnować?

Przede wszystkim użycie volatile ma znaczenie w modelu współbieżnym aplikacji. Kompilator dokonuje serii optymalizacji które na końcu mogą spowodować, że kod się nie zachowuje tak jak logicznie to wynika z kodu. Szczególnie ma to znaczenia dla komputerów z kilkoma rdzeniami lub procesorami.

Problemy z optymalizacją wynikają ze skomplikowanej architektury procesorów i tzw. modelu pamięci (memory model). Dla programistów, którzy pamiętają jeszcze Assembler będzie to dużo łatwiej zrozumieć. Aby nie komplikować, procesor operuje na danych z tzw. rejestrów a nie bezpośrednio na pamięci operacyjnej czy tym bardziej dysku twardym. Na przykład aby wykonać operację matematyczną najpierw procesor pobiera z pamięci liczbę i umieszcza ją w stosownym rejestrze. Wszelkie operacje dokonywane są właśnie na tym rejestrze (jest wiele typów rejestrów ale  o tym odsyłam do zewnętrznych zasobów). Po wykonaniu stosownej logiki, z powrotem wartość z rejestru jest przenoszona do pamięci. W świecie c# widzimy tylko zmienne ale w rzeczywistości nawet linia typu i++ powoduje przeniesie wartości z pamięci do rejestru, wykonanie operacji i z powrotem wynik ląduje w pamięci. Z tego względu zwykła  inkrementacja nie jest operacją atomową i należy do tego użyć klasy Interlocked.

Rozważmy przykładową optymalizację kodu dokonaną przez kompilator:

while( _flag )
{
    // do something
}

Jeśli w środku pętli nie modyfikujemy flagi _flag, optymalizacja dokonana przez kompilator mogłaby wyglądać tak:

if(_flag)
{
    while(true)
    {
        // do something
    }
}

Innym problemem są optymalizacje dokonywane przez sam CPU. Procesor cachuje wartość w rejestrze zamiast za każdym razem kopiować ją z pamięci operacyjnej. Generalnie dostęp do danych z dysku twardego jest bardzo kosztowny, dostęp do pamięci operacyjnej “średnio” a z kolei wszelkie operacje na rejestrach są bardzo wydajne. Naturalne wydaje się wiec unikanie operacji na RAM a zaplanowanie tak przepływu logiki aby uzyskać jak najmniej operacji kopiowania między RAM a rejestrami. Z tego względu procesor ma do dyspozycji cache i kolejne operacje odczytu wykonywane są na cachu a nie na pamięci operacyjnej. Mając w kodzie pętle while odpytującą flagę, kopiowana wartość jest tylko raz, następne wywołania dotyczą cachu rejestru. Z tego względu gdy inny wątek zmodyfikuje zmienną to wartość może nie być widziana przez inny procesor. Jest to trudne do zaobserwowania ponieważ zależy to od wspomnianego modelu pamięci i architektury procesora. Zasadniczo jednak istnieje ryzyko, że wartość zmodyfikowana przez inny wątek nie będzie widziana przez wszystkie pozostałe wątki gdy korzystają one z wartości buforowanej. Należy zaznaczyć, że wszelka operacja zapisu powoduje aktualizacje całego cache dostępnego dl wątku\rdzenia\cpu. Jest to przeciwne zachowanie do Javy, jednak w .NET operacja zapisu jest bezpieczna. Problem pojawia się wyłącznie w odczycie danych.

Rozważmy, żywy przykład. Od razu ostrzegam, że podany kod może nie zadziałać u wszystkich (zależne od procesora itp.). Należy program skompilować w trybie Release (aby zostały dokonane optymalizacje) i uruchomić koniecznie NIE z poziomu Visual Studio! Można np. przejść do katalogu Bin\Release. Podczas swoich testów również zaznaczyłem opcję x64. Kod (przykład poniższego programu jak i optymalizacji powyższego while pochodzi z stąd – polecam):

class Program
{
   bool flag=true;
   static void Main(string[] args)
   {
       Program test=new Program();
       var thread = new Thread(() => { test.flag = false; });
       thread.Start();

       while (test.flag)
       {
           // blokada
       }            
   }
}

Jaki z punktu logicznego myślenia powinien być wynik? Pętla while powinna zakończyć działanie po ustawieniu flagi na false przez inny wątek. Takie zachowanie na pewno można zaobserwować w trybie debug. W trybie release jednak odczyt test.flag będzie zbuforowany i zapis dokonany przez wątek nie będzie widoczny dla pętli while. Aby ominąć tę optymalizację i zawsze odczytywać z pamięci należy użyć słowa volatile:

volatile bool flag=true;
static void Main(string[] args)
{
  Program test=new Program();
  var thread = new Thread(() => { test.flag = false; });
  thread.Start();

  while (test.flag)
  {
      // blokada
  }            
}

Kod jest całkowicie bezpieczny – zawsze się zakończy. Jednak wprowadzenie volatile powoduje stratę jeśli chodzi o wydajność – za każdym razem w końcu dokonujemy flush. Jeśli piszemy aplikacje współbieżne należy mieć to na uwadze bo tam wydajność zwykle jest bardzo ważna.

Wiemy, że każdy write powoduje flush. Aby również read powodował flush (odświeżenie cachu) należy pole oznaczyć słowem volatile. Wszelki kod w Lock również jest bezpieczny i nie powoduje opisanych wcześniej problemów.

Należy również podkreślić, że volatile to nie operator do synchronizacji. Wszelkie operacje, które nie są atomowe nadal są niebezpieczne na polu volatile. Do synchronizacji mamy do dyspozycji lock, semafory i masę innych narzędzi dostarczonych przez .NET.

Code review: Instrukcje warunkowe

W programowaniu obiektowym użycie tak popularnej konstrukcji jak if-else jest często symptomem złej architektury. Załóżmy, że mamy metodę walidującą dostęp do danych np:

private bool Validate(string userName, string password, AuthType authType)
{
    if(authType == AuthType.PlainPwd)
    {
        if(password == _user.Password)
            return true;            
    }
    else if(authType == AuthType.MD5)
    {
        if(Md5.Hash(password) == _user.Password)    
    }
    else if(authType == ....)
    // ITD...

}

Powyższy kod łamie kilka zasad inżynierii oprogramowania np. Open\Closed Principle. Aby dodać nowy typ autoryzacji musimy dodać kolejny element do ENUM oraz zmodyfikować już istniejącą metodę. Przed postawieniem każdego IF’a w programowaniu obiektowym należy się sporo zastanowić. Często IF oznacza złamaną zasadę Open\Closed  i po prostu zapomnienie o polimorfizmie. Dużo lepszym podejściem jest stworzenie interfejsu IValidator a potem różne implementacje typu HotmailValidator, Md5PasswordValidator itp.

Co to zmieni? Dodając nowy typ walidacji, nie musimy modyfikować istniejącego kodu, który został już przetestowany. Dodanie nowej funkcjonalności typu autoryzacja na podstawie ról, także będzie łatwiejsza bo nie będziemy musieli zaśmiecać powyższej metody a kod będziemy mogli umieścić w konkretnej już klasie.

Kolejna sprawa to testy jednostkowe. Dysponując Inversion of Control łatwo będzie podstawić coś w stylu MockValidator. Nie musimy w końcu się łączyć realnym serwerem aby zweryfikować hasło podczas testów.

IF’y nie są elastycznym rozwiązaniem i często kończą się zaśmiecaniem kodu, który na końcu skutkuje spaghetti i długim szukaniem błędów. Sam IF jest często zaprzeczeniem zasady Single responsibility bo jeśli w tej samej klasie mamy różne problemy (concerns) tzn. że logika powinna zostać rozdzielona na więcej klas.

Oczywiście nie można uogólniać. Nie ma nic złego w sprawdzeniu czy parametr funkcji Divide jest różny od zero.  Chodzi raczej o serie IF’ów, np. w funkcji walidującej poprawność danych. Brzydkie rozwiązanie to seria IF’ów sprawdzającą długość, format itp. Poprawne rozwiązanie to np. z użycie wzorca chain of responsibility.

C#: unchecked i checked

Dziś następna porcja bardziej egzotycznych słów kluczowych w c#. Słowa unchecked oraz checked służą do kontrolowania czy nie nastąpił overflow podczas operacji arytmetycznych. Wszystkie niepoprawne operacje w klauzuli checked wywołają wyjątek overflow, ponieważ podczas wykonywania obliczeń sprawdzane jest czy wynik wciąż się mieści w zmiennej. Na przykład:

checked
{
    int i = 0;
    while (true)
        i++;
}

Po pewnym czasie, gdy zmienna i przekroczy Interger.Max, zostanie wyrzucony wyjątek.  .NET za każdym razem sprawdza czy zasięg został nieprzekroczony. Powoduje to oczywiście spadek wydajności. Bardziej wydajne rozwiązanie to:

unchecked
{
 int i = 0;
 while (true)
     i++;
}

Pętla nigdy się nie zakończy – wartość zostanie zawinięta po prostu do minimalnej liczby po przekroczeniu Int.Max. Oczywiście unchecked jest dużo szybsze niż checked. Można użyć składni typu unchecked(expression):

int i = 0;
i = unchecked(i++);

unchecked jest szybszy od checked, jednak należy pamiętaj o kilku sprawach:

  1. Domyślnie wszystkie operacje w C# są unchecked (nie trzeba owijać ich w konstrukcję unchecked).
  2. Unchecked używamy gdy w opcjach kompilatora ustawiliśmy checked ale mimo to, dla pewnych operacji chcemy ominąć sprawdzanie.
  3. unchecked\checked możemy zagnieżdżać w sobie. Np. owijamy całą funkcję  w unchecked  a potem pewne kwestie w checked.

Operacje na stałych, spowodują błąd już na etapie kompilacji, np:

int i = int.MaxValue + 1;

Owijając kod w unchecked, pozbywamy się “problemu”:

unchecked
{
    int i = int.MaxValue + 1;
}

Unchecked często używa się do liczenia hashy (GetHashCode)l. Przekroczenie zasięgu w pewnych algorytmach jest dopuszczalne. Jeszcze raz jednak zaznaczam, że dla nowego projektu w VS, unchecked to operacja domyślna i niejawna. Wspomniałem, że użycie unchecked ma sens głównie gdy globalnie np. checked jest ustawiony. Kiedy więc warto ustawić checked globalnie? Moim zdaniem można zastanowić się nad następującym scenariuszem:

  1. checked globalny ustawiony dla debug – ułatwia wyłapywanie błędów.
  2. checked usunięty w release – wydajność.

checked\unchecked działa tylko dla kodu zamieszczonego bezpośrednio w klauzuli. Wszelkie wywołania innych funkcji nie będą sprawdzane np:

checked
{
     Method(); // arytmetyka nie jest w Method sprawdzana
}
private void Method()
{
    int i = 0;
    while (true)
         i++;
}

Ponadto checked\unchecked dotyczy wyłącznie Int\Uint. Zasięg dla decimal jest zawsze sprawdzany (nawet w unchecked – nie ma to znaczenia).  Z kolei operacje na float\double nie są nigdy sprawdzane (ponieważ istnieją tam wartości takie jak Inf, Nan):

// unchecked nie ma znaczenia dla decimal - wyjatek zawsze zwracany
unchecked 
{
 decimal i = decimal.MaxValue;
 while (true)
     i *= 500;
}
// checked nie ma znaczenia dla float - nigdy nie jest sprawdzana arytmetyka
checked
{
 decimal i = decimal.MaxValue;
 while (true)
     i *= 500;
}

Kiedy ma sens wyłapywanie overflow exception? Istnieją algorytmy w których należy ignorować pewną operację gdy nastąpił overflow – musimy mieć więc jakiś sposób detekcji tego. Osobiście jednak jestem przeciwko wyłapywaniu wyjątków jako sposobowi na sterowanie logiki – można to wykonać w inny sposób.

Code review: Inicjalizacja obiektu, wywoływanie metody wirtualnej w konstruktorze

Witam, co powiedziecie na taki kod?

class Employee
{
   public Employee()
   {
       Init();
   }
   public virtual void Init()
   {
   }
}
class Manager : Employee
{
   public Manager()
   {

   }
   public override void Init()
   {       
   }
}

Czy jest to dobry design? Jeśli ktoś programował w C++, na pewno nie zgodzi się na wywoływanie jakiejkolwiek metody wirtualnej w konstruktorze. W CPP zostałaby wywołana metoda Employee:Init zamiast Manager:Init ponieważ w momencie tworzenia Employee, obiekt Manager jeszcze nie istnieje i dlatego nie może zostać wywołana Manager:Init. Myślę, że jest to bardzo intuicyjne. Obiekty są tworzone od tych podstawowych do tych najbardziej zagnieżdżonych. W momencie tworzenia rodzica, nie mamy dostępu do child’a. Wywoływanie metody wirtualnej jest zatem mylące, ponieważ wykonany zostanie kod, którego się nie spodziewaliśmy (Init Manager’a nigdy nie zostanie wykonany).

A jak to wygląda w przypadku C#? Konstruktory są również wywoływane od rodzica (Employee) do dzieci (Manager). Jednak w przeciwieństwie do CPP, metody wirtualne wywoływane są zawsze dla obiektów dziedziczących – czyli w tym przypadku zostanie wywołana prawidłowa metoda Manager:Init (a nie jak w CPP, Employee:Init). Również, w przeciwieństwie do CPP, wszelkie pola inicjalizowane w momencie deklaracji, będą dostępne z prawidłowymi wartościami w Init.

Czy to znaczy, że w CPP jest to bad practice, a w c# dopuszczalny? Nie, w obu językach opisana sytuacja jest zła i prezentuje brzydki kod. Wyobraźmy sobie taką modyfikację:

class Employee
{
   public Employee()
   {
       Log();
   }

   public virtual void Log()
   {
   }
}
class Manager : Employee
{
   private string _data;
   public Manager(int id)
   {
       // zaladuj dane na podstawie ID
       _data = repository.Get(id);
   }
   public override void Log()
   {
       Console.Write(_data);
   }
}

Co jeśli dodamy np. parametr do konstruktora aby zainicjalizować parę pól, które następnie metoda wirtualna będzie używać? Log zostanie wykonany przed inicjalizacją obiektu (wywołaniem właściwego konstruktora). Konstruktor jest w końcu odpowiedzialny za inicjalizację stanu obiektu i wykonywanie jakichkolwiek operacji na obiekcie, przed wywołaniem konstruktora jest niebezpieczne i może spowodować późniejszą niestabilnością obiektu.

Słowo kluczowe stackalloc

Dziś znowu zaprezentuję mało znane słowo kluczowe w języku c# – stackalloc.  Najpierw jednak kilka słów przypomnienia na temat alokacji pamięci w .NET.  Generalnie mamy dwa typy obiektów: reference type oraz value type. Typy referencyjne to klasy, z kolei value type to Enum, Integer, Float itp. Klasy alokowane są na stercie (heap), która zarządzana jest przez Garbage Collector. Value Type deklarowane są z kolei na zwykłym stosie. Wyjątkiem jest  sytuacja w której value type jest składową reference type (np. pole w klasie) –  wtedy będzie umieszczone na stercie

Domyślnie tablica danych umieszczana jest na stercie, a wskaźnik na nią na stosie. Można to zmienić wykorzystując słowo stackalloc:

unsafe
{
    int* array = stackalloc int[100];
}

Tablica array  zostanie w całości zadeklarowana na stosie. Wiąże się z tym kilka korzyści:

  1. Nie musimy martwić się, że GC zmieni adres obiektu – słowo kluczowe fixed jest całkowicie niepotrzebne. Adres na stercie jest zawsze taki sam. Czasami lepiej zadeklarować tablicę na stosie niż na stercie z fixed.
  2. Zwalnianie obiektów na stosie jest dużo szybsze – wystarczy, że metoda skończy swoje wykonywane a cały stos (zawierający parametry wejściowe itp.) zostanie po prostu wyczyszczony. Koniec problemów z fragmentacją, pinning itp.

Stos oczywiście nadaję się wyłącznie do małych i tymczasowych obiektów. Tak jak ze słowem fixed, rzadko w praktyce zdarza się konieczność jego zastosowania. Microsoft jednak jak widać dostarcza kilku “niskopoziomowych” mechanizmów na wszelki wypadek. Czasami aplikacja, która głównie wykonuje zarządzany kod, potrzebuje wykonać coś bardzo skomplikowanego i wtedy możemy właśnie skorzystać z sekcji unsafe.