Miłosz Orzeł

.net, js, html, arduino, java... no rants or clickbaits.

Modyfikator ref dla typów referencyjnych i odrobina SOSu

Spójrz na poniższy kod i zastanów się jaka wartość zostanie wyświetlona na konsoli (pamiętaj, że string to typ referencyjny)?

using System;
               
class Program
{
    static void Test(string y)
    {
        y = "bbb";
    }

    static void Main()
    {
        string x = "aaa";
        Test(x);
        Console.WriteLine(x);
    }
}

Prawidłowa odpowiedź (aaa) nie jest wcale taka oczywista. Użytkownik zobaczy napis aaa, dlatego, że bez użycia modyfikatora ref, program napisany w C# przekazuje do metody kopię wartości parametru (dla typów wartościowych) lub kopię referencji (dla typów referencyjnych).

Gdy do parametru y w metodzie Test przypisywany jest nowy tekst, CLR nie modyfkuje tablicy znaków. Zamiast tego tworzy nowy string (więcej info tutaj) i przypisuje wskazanie do niego do zmiennej y. Zmienna y znajdująca się w metodzie Test jest jednak tylko kopią referencji do napisu wskazywanego przez zmienną x z metody Main. Skoro zmodyfikowana została jedynie kopia, to po wyjściu z metody, na konsole trafia pierwotny napis aaa.

By rzeczywiście zmienić tekst kryjący się pod zmienną x, użyj modyfikatora ref (musisz dodać go zarówno w deklaracji metody jak i jej wywołaniu – C# wymusza takie zachowanie by zwiększyć czytelność kodu):

using System;
               
class Program
{
    static void Test(ref string y)
    {
        y = "bbb";
    }

    static void Main()
    {
        string x = "aaa";
        Test(ref x);
        Console.WriteLine(x);
    }
}

Po takiej zmianie na konsole trafi napis bbb.

 

SOS

Sposób przekazywania parametrów do metody można zbadać za pomocą narzędzia SOS (Son of Strike). Posłużymy się poleceniem CLRStack -a, które wyświetli informacje o parametrach i zmiennych lokalnych na stosie kodu zarządzanego (jeśli nie wiesz jak używać SOS patrz tutaj i tutaj, jeśli dziwisz się skąd nazwa "Son of Strike" kliknij tu)...

Poniżej znajdują się rezultaty polecania CLRStack -a, wykonanego w momencie wejścia do metody Test.

Dla kodu bez modyfikatora ref:

!CLRStack -a
OS Thread Id: 0x176c (5996)
Child SP IP       Call Site
0031f114 00390104 Program.Test(System.String)
    PARAMETERS:
        y (0x0031f114) = 0x025cb948

0031f158 003900af Program.Main()
    LOCALS:
        0x0031f158 = 0x025cb948

0031f3c0 656721bb [GCFrame: 0031f3c0]

Dla kodu z modyfikatorem ref:

!CLRStack -a
OS Thread Id: 0x934 (2356)
Child SP IP       Call Site
001dee34 002f00f4 Program.Test(System.String ByRef)
    PARAMETERS:
        y (0x001dee34) = 0x001dee78

001dee78 002f00aa Program.Main()
    LOCALS:
        0x001dee78 = 0x027fb948

001df0ec 656721bb [GCFrame: 001df0ec]

Istotną różnicą widoczną na powyższych zrzutach jest wartość parametru y. W przypadku kodu bez modyfikatora ref jest to adres stringa aaa (0x025cb948), natomiast dla kodu z modyfikatorem ref, wartością parametru y jest adres zmiennej x z metody Main (0x001dee78)która wskazuje na string aaa.

BadImageFormatException, x86 i x64

Czy natrafiłeś kiedyś na wyjątek typu BadImageFormatException lub komunikat "An attempt was made to load a program with an incorrect format" ("Próbowano załadować program w niepoprawnym formacie")?

Jeśli tak to być może program, który próbowałeś uruchomić nie został skompilowany z użyciem opcji /platform:x86. Zastanawiasz się pewnie dlaczego podczas programowania w C# powinieneś przejmować się tym na jakiej platformie (x86/x64) kod zostanie wykonany. Cóż, w większości przypadków nie musisz o tym myśleć. Jeśli nie używasz bloków unsafe ani nie importujesz natywnych modułów problemu nie ma, bowiem Twój kod C# zostaje przetłumaczony do kodu pośredniego (CIL), który przed wykonaniem zostaje skompilowany (JIT) na postać odpowiednią dla platformy docelowej. Ok, ale...

Wyobraź sobie, że używasz w swojej aplikacji funkcji importowanej z 32 bitowej DLL. Gdy uruchomisz program na 32 bitowym systemie wszystko działa jak należy. Niestety na maszynie x64 otrzymujesz wspominany wyjątek BadImageFormatException. Dlaczego? Jeśli assembly importujące DLL zostało skompilowane z opcją /platform:anycpu to do jego uruchomienia na systemie 64 bitowym została użyta 64 bitowa wersja CLR. Próba załadowania 32 bitowej DLL z aplikacji działającej w procesie 64 bitowym nie powiedzie się. Gdyby do kompilacji została użyta opcja /platform:x86 wówczas na 64 bitowym systemie operacyjnym program zostałby uruchomiony w 32 bitowej wersji CLR (z użyciem WoW64: Windows 32-bit on Windows 64-bit).

Platformę docelową (przełącznik /platform kompilatora C#) można w Visual Studio 2010 ustawić w oknie “Properties” na zakładce “Build”. By tam trafić kliknij prawym klawiszem w plik projektu w Solution Explorer i wybierz „Properties” lub użyj menu głównego „Project | <nazwa projektu> Properties…”.

Ustawienie platformy docelowej w Visual Studio 2010. Kliknij aby powiększyć...

Microsoft stworzył przydatne narzędzie wiersza poleceń o nazwie CorFlags, które służy między innymi do podglądu lub ustawiania platformy docelowej. Dostęp do tego narzędzia można uzyskać korzystając z Visual Studio Command Prompt albo przez znalezienie go bezpośrednio na dysku (u mnie jesto ono pod C:\Program Files\Microsoft.NET\SDK\v2.0\Bin\CorFlags.exe)

Poniżej znajduje się kilka przykładów tego co możesz zobaczyć po sprawdzeniu plików EXE stworzonych z różnymi wartościami opcji /platform kompilatora (do sprawdzenia pliku służy polecenie: CorFlags nazwa.pliku):

anycpu x86 x64
Version   : v4.0.30319
CLR Header: 2.5
PE        : PE32
CorFlags  : 1
ILONLY    : 1
32BIT     : 0
Signed    : 0
Version   : v4.0.30319
CLR Header: 2.5
PE        : PE32
CorFlags  : 3
ILONLY    : 1
32BIT     : 1
Signed    : 0
Version   : v4.0.30319
CLR Header: 2.5
PE        : PE32+
CorFlags  : 1
ILONLY    : 1
32BIT     : 0
Signed    : 0

W kontekście tego posta istotne są 2 rzędy z wyniku zwróconego przez CorFlags: PE i 32BIT.

  • PE: PE32 oznacza, że plik może być wykonany w środowisku x86 i x64
  • PE: PE32+ oznacza, że plik może być wykonany jedynie w środowisku 64 bitowym
  • 32BIT: 1 oznacza, że program musi być wykonany w środowisku x86

Zrozumienie znaczenia 32BIT: 1 jest naprawdę istotne jeśli chcesz uniknąć problemów z importowanie 32 bitowch DLL na 64 bitowej wersji Windows. Jeśli flaga 32BIT jest ustawiona i uruchomisz plik PE32 na x64 wówczas Twoja aplikacja zostanie uruchomiona w środowisku 32 bitowym (z użyciem WoW), dzięki czemu zaistnieje możliwość zaimportowania 32 bitowej DLL. Jeśli flaga 32BIT nie jest ustawiona, aplikacja uruchomi się w procesie 64 bitowym – co spowoduje problem z załadowaniem biblioteki.

Dzięki CorFlags można w łatwy sposób zmodyfikować wartość flagi 32BIT. Do jej ustawienia służy przełącznik /32BIT+

CorFlags file.exe /32BIT+

A do jej usuwania /32BIT-

CorFlags file.exe /32BIT-

Tak więc nawet jeśli nie masz możliwości przekompilowania problematycznego kodu z odpowiednią opcją /platform nadal możesz użyć 32 bitowej DLL w 64 bitowej wersji systemu Windows :)

Dlaczego użycie GetPixel i SetPixel jest tak bardzo nieefektywne?

Klasa Bitmap dostarcza dwie proste metody: GetPixel i SetPixel służące odpowiednio do pobierania koloru punktu obrazu (jako struktury Color) oraz ustawienia punktu obrazu. Poniższy kod ilustruje sposób pobrania/ustawiania wszystkich pikseli bitmapy:

private void GetSetPixel(Bitmap image) {
   for (int x = 0; x < image.Width; x++) {
      for (int y = 0; y < image.Height; y++) {
         Color pixel = image.GetPixel(x, y);
         image.SetPixel(x, y, Color.Black);
      }
   } 
}

Jak widać przeglądnięcie i modyfikacja pikseli jest niezwykle prosta. Niestety za prostotą kodu kryje się poważna pułapka wydajnościowa. O ile dla niewielkiej ilości odwołań do punktów obrazu, prędkość z jaką działają metody GetPixel i SetPixel jest zadowalająca, o tyle dla większych rozmiarów obrazu jest ona zdecydowanie za mała. Za dowód może posłużyć wykres z wynikami 10 testów*, które polegały na 10-krotnym wywołaniu w/w metody GetSetPixel na obrazach 100x100 i 1000x1000 pikseli:

Wyniki testów prędkości operacji na pikselach obrazu z użyciem metod GetPixel i SetPixel klasy Bitmap.

Średni czas testu dla obrazu o wymiarach 100 na 100 pikseli wyniósł 543 milisekundy. Taka wydajność jest możliwa do zaakceptowania o ile przetwarzanie obrazu nie będzie wykonywane często. Problem wydajnościowy jest natomiast jasno widoczny przy próbie obsługi obrazu o rozmiarach 1000 na 1000 pikseli. Wykonanie testu zabiera w tym przypadku średnio ponad 41 sekund – ponad 4 sek. na jedno wywołanie GetSetPixel (sic!) .


Dlaczego tak wolno?

Niska wydajność spowodowana jest tym, że dostęp do piksela nie jest prostym odwołaniem do obszaru pamięci. Każde pobranie lub ustawienie koloru wiąże się z wywołaniem metody .NET Framework, będącej oprawą dla natywnej funkcji zawartej w bibliotece gdiplus.dll. Wywołanie to następuje za pomocą mechanizmu P/Invoke (Platform Invocation), który służy do komunikacji kodu zarządzanego z API niezarządzanym (API z poza .NET Framework). Tak więc np. dla bitmapy o rozmiarze 1000x1000 pikseli dojdzie do miliona wywołań metody GetPixel, która prócz walidacji parametrów korzysta z funkcji natywnej GdipBitmapGetPixel. Metoda z API GDI+ musi z kolei przed zwróceniem informacji o kolorze wykonać takie operację jak np. wyliczenie położenia bajtów odpowiedzialnych za opis szukanego piksela... Sytuacja analogiczna zachodzi w przypadku metody SetPixel.

Spójrz na poniższy kod metody Bitmap.GetPixel uzyskany dzięki .NET Reflector (System.Drawing.dll, .NET Framework 2.0):

public Color GetPixel(int x, int y) {
   int argb = 0;
   if ((x < 0) || (x >= base.Width)) {
      throw new ArgumentOutOfRangeException(“x”, SR.GetString(“ValidRangeX”));
   }
   if ((y < 0) || (y >= base.Height)) {
      throw new ArgumentOutOfRangeException(“y”, SR.GetString(“ValidRangeY”));
   }
   
   int status = SafeNativeMethods.Gdip.GdipBitmapGetPixel(new HandleRef(this, base.nativeImage), x, y, out argb);
   if (status != 0) {
      throw SafeNativeMethods.Gdip.StatusException(status);
   }
   return Color.FromArgb(argb);
}

A oto import funkcji z GDI+:

[DllImport(“gdiplus.dll”, CharSet=CharSet.Unicode, SetLastError=true, 
ExactSpelling=true)]
internal static extern int GdipBitmapGetPixel(HandleRef bitmap, int x, int y, out int argb);

Teraz już wiesz dlaczego masowe użycie Get/SetPixel jest takie powolne. Na szczęście istnieją inne (dużo szybsze) sposoby obsługi pikseli z poziomu .NET. Przy pewnym wysiłku można napisać kod, który szybciej obsłuży obraz megapikselowy niż prymitywna metoda poradzi sobie z bitmapą 100x100!. Ale o tym, gdy znajdę nieco czasu... ;)

Aktualizacja 2013-11-07: Napisałem artykuł o szybkich operacjach pikselowych. Koniec z ślamazarnym Get/SetPixel :)

Aktualizacja 2018-01-08: Jeśli zależy Ci na naprawdę wydajnym przetwarzaniu obrazu powinieneś sięgnąć po wyspecjalizowaną bibliotekę, taką jak OpenCV. Kilka miesięcy temu napisałem serie postów "Detecting a Drone - OpenCV in .NET for Beginners (Emgu CV 3.2, Visual Studio 2017)", które pomogą Ci zacząć...

* Testowałem na takim laptopie: HP Pavilion dv5, procesor AMD Turion X2 Dual-Core Mobile RM-70, 3 GB RAM, Vista Home Premium

Dlaczego stringi są niezmienne i co z tego wynika?

 <suchar>Niestety zajmiemy się mniej namacalną implementacją stringów</suchar>  ;)

Typ string (System.String) służy do reprezentacji łańcuchów tekstowych w postaci sekwencji wartości typu char (System.Char) określających znaki Unicode (zakodowane w UTF-16). Zazwyczaj jeden element char koduje jeden znak...

Przy pracy z ciągami tekstowymi należy pamiętać, że stringi w .NET są niezmienne (immutable)! Oznacza to po prostu, że raz utworzony łańcuch tekstowy nie może być modyfikowany (bez użycia refleksji lub kodu unsafe), a metody pozornie modyfikujące string, tak naprawdę zwracają nowy obiekt mający żądaną wartość.

Niezmienność stringów ma wiele zalet (o tym za chwilę), może jednak być źródłem problemów jeśli programista zapomni, że każda "zmiana" łańcucha tekstowego to tak naprawdę tworzenie nowej instancji klasy String. Chociaż CLR traktuje stringi w szczególny sposób, nadal są one typem referencyjnym, dla których to pamięć przydzielana jest na zarządzanej stercie.

Działanie tej pętli spowoduje utworzenie 10 tysięcy zmiennych typu string, z których wszystkie prócz ostatniej to śmieci, które będzie musiał zebrać Garbage Collector:

string s = string.Empty;

for (int i = 0; i < 10000; i++)
{
    s += "x";
}

Na poniższym obrazie widać fragment okna "Histogram by Size for Allocated Objects" aplikacji CLR Profiler. Widać jak kolejne obroty pętli powodowały alokacje pamięci na stercie dla coraz większych stringów:

By uniknąć tworzenia wielu niepotrzebnych obiektów należy użyć klasy StringBuilder, która umożliwia modyfikację tekstu bez każdorazowego tworzenia nowych obiektów:

StringBuilder sb = new StringBuilder();

for (int i = 0; i < 10000; i++)
{
    sb.Append("x");
}

string x = sb.ToString();

Ta prosta zmiana ma ogromny wpływ na ilość przydzielanej i zwalnianej pamięci. Spójrz na poniższe porównanie fragmentów okien "Summary" profilera:

 

Dlaczego projektanci .NET (tak samo jak np. twórcy Javy) zdecydowali się zaimplementować niezmienne łańcuch tekstowe?

Z przyczyn optymalizacyjnych (głównie ze względu na szybkość porównywania) łańcuchy tekstowe mogą być zapisywane w specjalnej tabeli (intern pool) utrzymywanej przez CLR. Chodzi o to by uniknąć tworzenia wielu zmiennych określających ten sam ciąg znaków. Poniżej przedstawiony jest kawałek kodu dowodzący tego, że zmienne posiadające ten sam łańcuch tekstowy mogą wskazywać na ten sam* obiekt:

string a = "xx";
string b = "xx";
string c = "x";
string d = String.Intern(c + c);

Console.WriteLine((object)a == (object)b); // True
Console.WriteLine((object)a == (object)d); // True

Gdyby łańcuchy tekstowe były modyfikowalne, zmieniając wartość zmiennej a, zmieniłoby się też wartość b oraz d.

Niezmienność stringów ma też pozytywne znaczenia w aplikacjach wielowątkowych - zmiana tekstu to utworzenie nowej zmiennej więc nie ma konieczności zakładania blokady (lock) w celu uniknięcia konfliktów związanych z jednoczesnym dostępem wielu wątków do jednego łańcucha tekstowego. Ma to tym większe znaczenie, że często dokonuje się autoryzacji pewnych operacji w zależności od tekstu (np. można blokować szczególne funkcjonalności usługi bazując na stringowym adresie klienta).

Ważnym powodem niezmienności jest powszechne użycie stringów jako kluczy w tablicach haszujących. Jaki sens miało by wyliczenie położenia elementu w tablicy, gdyby można było zmodyfikować wartość użytego klucza? Poniższy listing dowodzi, że "zmiana" wartości zmiennej użytej jako klucz nie ma wpływu na działanie tablicy:

string key = "abc";
Hashtable ht = new Hashtable();
ht.Add(key, 123);

key = "xbc";

Console.WriteLine(key); // xbc
Console.WriteLine(ht["abc"]); // 123

O tym co stałoby się w przypadku modyfikowalnych łańcuchów tekstowych można się przekonać za pomocą kodu używającego bloku unsafe w celu rzeczywistej zmiany wartości stringa użytego jako klucz:

unsafe
{
    string key = "abc";
    Hashtable ht = new Hashtable();
    ht.Add(key, 123);

    fixed (char* p = key)
    {
        p[0] = 'x';
    }

    Console.WriteLine(key); // xbc
    Console.WriteLine(ht["abc"]); // Nie znajdzie!
}

Niezmienność wiąże się też z tym, że string wewnętrznie przechowywany jest jako tablica, czyli struktura danych reprezentującą ciągłą przestrzeń adresową (dla której ze względów wydajnościowych nie dopuszcza się np. operacji wstawiania elementu).

* To czy literał łańcuchowy jest automatycznie dodawany do puli może być zależne od użycia narzędzia Ngen.exe lub ustawień CompilationRelaxations...

Wstęp do wyrażeń regularnych na platformie .NET.

Celem artykułu jest zapoznanie Czytelnika z podstawowymi elementami składni wyrażeń regularnych oraz sposobami ich wykorzystania na platformie .NET. Postaram się zachęcić tych, którzy jeszcze nie mieli do czynienia z regexami do poznania tego potężnego narzędzia.

Wersja: 1.1 (05.2007, poprawki 07.2007)
Poziom trudności: początkujący
Uwagi: Artykuł ten pisałem w ramach nauki .NET Framework jako pracę uczestniczącą w konkursie portalu CodeGuru.pl (tekst zajął drugie miejsce w maju 2007 r.).

Artykuł w PDF (v1.1 – 07.2007):
http://morzel.net/download/wdre_art.pdf

Artykuł na CodeGuru.pl (v1.0 – 05.2007):
http://www.codeguru.pl/article-696.aspx

Załączony kod (C#, VS 2005):
http://morzel.net/download/wdre_src.zip