ASP.NET MVC–TempData

ASP.NET MVC dostarcza wiele kontenerów na dane m.in. ViewBag czy ViewData. Istnieje jeszcze do dyspozycji TempData, który czasami może przydać się. Zacznijmy od przykładu:

public class HomeController : Controller
{
   public ActionResult Index()
   {
       TempData["Message"] = "test";
       return RedirectToAction("Display");
   }
   public ActionResult Display()
   {
       string msg = (string)TempData["Message"];
       return View();
   }
}

TempData działa bardzo podobnie do sesji. Pozwala przechowywać dane po stronie serwera, co jest przydatne w niektórych scenariuszach, gdy potrzebujemy przekazać dane pomiędzy zapytaniami.

Jaka sama nazwa jednak mówi, TempData służy do przechowywania tymczasowych wartości, które potrzebujemy na krótki czas. Ze względu, że dane w TempData zostaną zniszczone bardzo szybko, powinniśmy używać ich wyłącznie w następnym zapytaniach(np. poprzez Redirect).

Ponadto, próba odczytu danych automatycznie niszczy wpis. Raz odczytana wartość z TempData, zostanie zniszczona i nie będzie już dostępna w następnych zapytaniach. Jeśli chcemy przeczytać TempData w kilku akcjach należy użyć metody Keep:

TempData.Keep("Message");

W przeciwnym wypadku, odczyt za pomocą TempData[“Message”] po prostu unieważni wpis.

Powyższe uwagi oznaczają, że można mieć łańcuch akcji i w każdej z nich korzystać z TempData, pod warunkiem, że Keep zostanie wywoływany.

TempData może przechowywać każdy typ, który wspiera serializacje. Należy pamiętać, że sesja może również być przechowywana w bazie danych, stąd warto mieć na uwadze co przechowujemy w TempData.

Podpis cyfrowy oraz weryfikacja pakietu

W .NET bardzo łatwo podpisać cyfrowo jakiś pakiet. Przeważnie korzystając z WCF czy z innych technologii jest już to wykonane za nas w ramach framework’a. Bardzo często jednak pisząc aplikacje, chcemy zapewnić integralność danych. Za pomocą kilku linii kodu można tego dokonać. Przede wszystkim warto przyjrzeć się klasom RSACryptoServiceProvider  oraz DSACryptoServiceProvider. Obie posiadają one metody takie jak SignData oraz VerifyData.

Przyjrzymy się najpierw samemu podpisowi:

private static byte[] SignData(byte[] data,out RSAParameters cspParameters)
{
  var rsaCryptoServiceProvider=new RSACryptoServiceProvider();
  cspParameters = rsaCryptoServiceProvider.ExportParameters(false);
  return rsaCryptoServiceProvider.SignData(data, new SHA1Managed());            
}

Co metoda SignData robi? Najpierw wykonuje funkcję haszującą na danych, za pomocą algorytmu przekazanego jako drugi parametr. Potem używając klucza prywatnego, szyfruje dany hash. Zaszyfrowany kluczem prywatnym skrót stanowi podpis. Następnie możemy sprawdzić, czy przekazane nam dane, rzeczywiście pokrywają się z podpisem:

private static bool VerifyData(byte[] data, byte[] signature, RSAParameters cspParameters)
{
  var rsaCryptoServiceProvider = new RSACryptoServiceProvider();
  rsaCryptoServiceProvider.ImportParameters(cspParameters);
  return rsaCryptoServiceProvider.VerifyData(data, new SHA1Managed(), signature);
}

Aby zweryfikować integralność danych należy najpierw wyliczyć skrót przekazanego pakietu. Następnie, korzystając z klucza publicznego, odszyfrowujemy podpis. Jeśli podany hash (z podpisu) pokrywa się z tym co sami wyliczyliśmy to znaczy, że pakiet nie został po drodze zmodyfikowany oraz należy do tej osoby, która wydała nam klucz publiczny.

Całość:

private static void Main(string[] args)
{
  string text = "Hello World";
  byte[] data = Encoding.ASCII.GetBytes(text);

  RSAParameters rsaParameters;
  byte[] signature = SignData(data, out rsaParameters);
  Console.WriteLine(VerifyData(data, signature, rsaParameters));
}
private static bool VerifyData(byte[] data, byte[] signature, RSAParameters cspParameters)
{
  var rsaCryptoServiceProvider = new RSACryptoServiceProvider();
  rsaCryptoServiceProvider.ImportParameters(cspParameters);
  return rsaCryptoServiceProvider.VerifyData(data, new SHA1Managed(), signature);
}

private static byte[] SignData(byte[] data,out RSAParameters cspParameters)
{
  var rsaCryptoServiceProvider=new RSACryptoServiceProvider();
  cspParameters = rsaCryptoServiceProvider.ExportParameters(false);

ASP.NET MVC: RouteUrl a Action

Przez ostatnie lata nie zajmowałem zbytnio się aplikacjami webowymi ale stopniowo zmienia się to. Z tego względu więcej na blogu można spodziewać się takiej tematyki. Powoli sobie wszystko przypominam. Prawdopodobnie dla wielu z Was będą to podstawy ale mam nadzieję, że komuś przydadzą się takie wpisy.

W poście chciałbym pokazać czym różni się RouteUrl od Action. Zacznijmy następującej metody:

public ActionResult Index()
{
  string action = Url.Action("Index");
  string route = Url.RouteUrl("CustomRoute2");

  Response.Write(string.Format("Action: {0}<br/>",action));
  Response.Write(string.Format("Action: {0}", route));

  return View();
}

Action służy do generowania linku na podstawie przekazanych danych o akcji, kontrolerze itp. Route robi dokładnie to samo, z tym, że można przekazać dokładnie z jakiego route ma korzystać. Action wybierze po prostu pierwszy zdefiniowany routing. Przyjrzyjmy się naszej konfiguracji:

routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

routes.MapRoute("CustomRoute1", "OldApp/{action}/{id}", new {controller = "Home", action = "Index", id = UrlParameter.Optional});
routes.MapRoute("CustomRoute2", "App/{action}/{id}", new { controller = "Home", action = "Index", id = UrlParameter.Optional });

routes.MapRoute(
 "Default", // Route name
 "{controller}/{action}/{id}", // URL with parameters
 new { controller = "Home", action = "Index", id = UrlParameter.Optional } 
);

Pierwsza dopasowana to CustomRoute1. Zatem Action wyświetli OldApp/Nazwa Kontrolera/Nazwa Akcji. Z kolei do wywołania RouteUrl przekazujemy dokładnie nazwę konfiguracji, co oznacza, że zostanie wygenerowany link w postaci App/. Możliwe oczywiście jest korzystanie z tych funkcji w widoku. Analogicznie istnieje funkcja RouteLink, która wykonuje podobne zadanie co ActionLink, z tym, że wygenerowany link wykorzystuje konkretny routing.

Warto zaznaczyć, że lepiej unikać generowania linków dla specyficznego routingu. W kodzie unikamy ręcznego wpisywania <a href …/>, aby nie być zależnym od routingu. ActionLink wygeneruje link poprawny dla danej konfiguracji. Jeśli zmienimy zatem routing w międzyczasie, ActionLink odzwierciedli to  w przeciwieństwie do sztywno przypisanego <a href…/>.

Wydajność: Multicore JIT w .NET 4.5

Uruchomienie aplikacji .NET może być procesem powolnym. Związanie jest to oczywiście z potrzebą skompilowania kodu do postaci maszynowej. W przeciwieństwie do np. C++, w plikach jest przechowywany kod tymczasowy. W większości przypadków nie jest to problemem, ale dla naprawdę dużych aplikacji może być to odczuwalne. Szczególnie ma to znaczenie dla ASP.NET, gdzie zależy nam jak na najkrótszym opóźnieniu.

Jednym ze sposobów jest użycie nGen, który wygeneruje kod maszynowy. Ma to taką wadę, że trzeba wywołać go np. w instalatorze, ponieważ kod maszynowy jest oczywiście generowany dla konkretnego komputera.

Multicore JIT to z kolei zoptymalizowana kompilacja. Zasada jest bardzo prosta. Zamiast dokonywać kompilacji na jednym rdzeniu (i blokując MainThread), praca jest odpowiednio rozdzielana między CPU. Przynosi to dość duże korzyści, nawet 50%. Szczególnie aplikacje takie jak ASP.NET czy WPF zyskają na tym.

Pozostaje pytanie, jak metody są kompilowane. W podejściu sekwencyjnym, metoda jest kompilowana kiedy ma zostać wywołana pierwszy raz. W przypadku Multicore, z góry musimy znać całą ścieżkę, aby móc rozdzielić odpowiednio pracę. Z tego względu MultiCore działa w dwóch trybach. Gdy aplikacja jest uruchamiana pierwszy raz, wtedy cała ścieżka jest “nagrywana” i zapisywana do pliku. Podczas kolejnych uruchomień, profil ścieżki jest ładowany i może zostać już dokonana kompilacja równoległa. Warto zaznaczyć, że można przechowywać profile w różnych plikach. Jest to przydatne, gdy przekazywane są parametry uruchomieniowe, od których zależy konkretnie jaka seria metod będzie wywoływana.

Przejdźmy do kodu:

public static void Main() 
{
    ProfileOptimization.SetProfileRoot(@"C:\Test");
    ProfileOptimization.StartProfile("Startup.Profile1");
}  

Pierwsza metoda określa lokalizację (folder) w którym będą znajdowały się profile. Druga z kolei rozpoczyna konkretny profil.

Załóżmy, że mamy parametry uruchomieniowe i w zależności od nich, różne metody muszą zostać skompilowane:

public static void Main(string[] args) 
{
    System.Runtime.ProfileOptimization.SetProfileRoot("c:\\test");

    if (args[0] == "1") 
    {
        System.Runtime.ProfileOptimization.StartProfile("Profile1");
    } 
    else if (args[0] == "2") 
    {
        System.Runtime.ProfileOptimization.StartProfile("Profile2");
    }
}

Wszystkie aplikacje ASP.NET mają domyślnie aktywny multicore jit i nie trzeba nic konfigurować. Jeśli chcemy z kolei wyłączyć go, możemy to zrobić za pomocą web.config:

<?xml version="1.0" encoding="utf-8" ?> 
<configuration>
 <!-- ... -->
 <system.web> 
 <compilation profileGuidedOptimizations="None" /> 
 <!-- ... --> 
 </system.web> 
</configuration>

Multicore, jak wspomniałem, ma znaczenie dla rozbudowanych aplikacji. Optymalizacja jest dość znacząca ponieważ wynosi 30-50%.

Wydajność: Pętle oraz sprawdzenie indeksu

Dziś kolejny wpis o wydajności C#. Od razu chcę podkreślić, że należy traktować post jako ciekawostkę, która w zdecydowanej większości nie ma znaczenia. Stosując podane wskazówki można nieco przyśpieszyć algorytm, ale DUŻO lepsze efekty można uzyskać np. unikając błędnej alokacji obiektów, która ma fatalne efekty na GC.

Warto jednak znać zasady języka, w którym codziennie pisze się aplikacje. W świecie wysokopoziomowych języków, gdzie wszystko jest robione za nas, łatwo zapomnieć o rzeczach, które są oczywiste dla programistów ASM.

Co się dzieje, gdy wykonamy następującą linię kodu?

int data = items[index];

Wygenerowany kod, sprawdzi najpierw czy indeks nie przekracza dozwolonego zasięgu tzn. musi być >= 0 i < items.Length. Jeśli zasięg jest niedozwolony to zostanie wyrzucony wyjątek. Dzięki temu, nie będzie czytana wartość, która jest poza tablicą. W przypadku, gdy items ma 3 elementów a my chcielibyśmy przeczytać czwarty, bez sprawdzania uzyskalibyśmy dostęp do pamięci, przechowującej kompletnie inne informacje. Próba zapisu mogłaby skończyć się nadpisaniem jakiś innych, cennych informacji.

Oczywiście sprawdzanie warunku powoduje wygenerowanie kilku dodatkowych instrukcji. Programiści wysokopoziomowych języków nie przejmują się tym, ale gdybyśmy mieli korzystać tylko z ASM, zauważylibyśmy jak wiele niepotrzebnego kodu trzeba pisać. Problem pojawia się w przypadku pętli. Jeśli mamy kilka milionów iteracji, wtedy sprawdzanie tego samego warunku nie zawsze ma sens. JIT jest na tyle inteligentny, że często wygeneruje warunek wyłącznie przed pętlą np.:

for (int j = 0; j < data.Length; j++)
{
    int item = data[j];
}

Bardzo jednak łatwo napisać kod, który w każdej iteracji będzie sprawdzał indeks. W powyższym przykładzie, JIT ominie warunek w każdej iteracji bo nie ma przecież potrzeby sprawdzania tego za każdym razem – wiadomo, że indeks zawsze będzie miał prawidłową wartość. Spójrzmy na nieco inny przypadek:

const int n = 100000000;
int[] data = new int[n];
const int tests = 10;

for (int i = 0; i < tests; i++)
{
 Stopwatch stopwatch = Stopwatch.StartNew();

 for (int j = 0; j < data.Length; j++)
 {
     int item = data[j];
 }

 Console.WriteLine("1: {0}", stopwatch.ElapsedMilliseconds);
}

for (int i = 0; i < tests; i++)
{
 Stopwatch stopwatch = Stopwatch.StartNew();

 for (int j = data.Length - 1; j >= 0; j--)
 {
     int item = data[j];
 }

 Console.WriteLine("2: {0}", stopwatch.ElapsedMilliseconds);
}

Powyższy kod wyświetli czas potrzebny na przejście pętli. Pierwsza to klasyczna pętla, która zostanie zoptymalizowana. Druga z kolei, będzie przechodzić od końca do początku. Niestety nie zostanie dokonana żadna optymalizacja w drugim przypadku:

image

Z tego względu, lepiej pisać pętle, które mają porządek rosnący.

A co w przypadku pętli, gdzie zamiast właściwości Length, korzystamy ze stałej?

for (int j = 0; j < n; j++)
{
    int item = data[j];
}

Dla x86, warunek będzie sprawdzamy w każdej iteracji. Z kolei w przypadku x64 zostanie dokona optymalizacja.

Inny scenariusz, który nie zostanie zoptymalizowany to dostęp do statycznej tablicy:

private static []_data=new int[n];

//...

for (int j = 0; j < _data.Length; j++)
{
    int item = _data[j];
}

Ma to dobre wytłumaczenie ponieważ statyczna zmienna jest globalna i mogłaby być zmodyfikowana z innego wątku.

Można bez problemu ustawić początek i koniec pętli, a mianowicie:

for (int j = 50; j < data.Length-10; j++)
{
    int item = data[j];
}

JIT rozpozna taką pętle i zoptymalizuje ją. Nie trzeba zawsze zaczynać od indeksu 0. Kolejny przykład:

for (int j = 0; j < data.Length-1; j++)
{
    int item = data[j+1];
}

Pętla nieco inna ale wciąż jest optymalna – zostanie warunek sprawdzony tylko raz.

Powyższe rozważania dotyczą również tablic wielowymiarowych, które m.in. z tego względu są wolniejsze i lepiej korzystać z jagged array. Nawet w podstawowych, prostych pętlach warunek nie jest eliminowany dla tablic wielowymiarowych.

Generalnie bardzo łatwo napisać pętlę, która nie zostanie zoptymalizowana ale w praktyce nie ma to wielkiego znaczenia – czysta ciekawostka. 

Wydajność: Przeglądanie elementów w List oraz LinkedList

Kilka tygodni temu, w jednym z wpisów, porównałem wydajność List z LinkedList. Przykład udowodnił, że dodawanie nowych elementów w LinkedList potrafi być nawet wolniejsze niż w przypadku List. Bardzo często, programiści myślą, że to LinkedList jest lepszy do dodawania nowych elementów, ponieważ łatwiej doczepić nowy wskaźnik niż alokować ponownie pamięć (też tak kiedyś uważałem). W przypadku List jest to jednak nie do końca prawda, ponieważ List<T> alokuje  pamięć z góry (zawsze więcej niż aktualnie jest potrzebne), co powoduje, że dodanie nowego elementu ma złożoność również O(1). W przypadku List nie musimy  również tworzyć wrappera w formie LinkedListNode, co ma znaczenie dla value type.

Dzisiaj jednak kolejny przykład, pokazujący przewagę List. Zacznijmy po prostu od kodu:

private static void Main(string[] args)
{            
  var data = Enumerable.Range(0, 10000000).ToArray();
  const int tests = 10;

  TestLinkedList(tests,data);
  TestList(tests, data);
}

private static void TestLinkedList(int tests, int[] data)
{
  var linkedList = new LinkedList<int>(data);

  for (int i = 0; i < tests; i++)
  {
      Stopwatch stopwatch = Stopwatch.StartNew();

      LinkedListNode<int> node = linkedList.First;
      int sum = 0;

      while (node != null)
      {
          sum += node.Value;
          node = node.Next;
      }

      Console.WriteLine("LinkedList: {0}", stopwatch.ElapsedMilliseconds);
  }
}
private static void TestList(int tests, int[] data)
{
  var list = data.ToList();

  for (int i = 0; i < tests; i++)
  {
      Stopwatch stopwatch = Stopwatch.StartNew();
      
      int sum = 0;
      
      for (int j = 0; j < list.Count; j++)
      {
          sum += list[j];
      }

      Console.WriteLine("List: {0}", stopwatch.ElapsedMilliseconds);
  }
}

Wynik:

image

Różnica w trybie Release jest dość znacząca. Jak można to wytłumaczyć? W końcu mierzymy tutaj tylko przeglądanie elementów a nie alokowanie pamięci. W obydwu przykładach przechodzimy wyłącznie jeden raz, nie alokując zbędnych zmiennych lokalnych. Ponadto, LinkedListNode jest typem referencyjnym więc nie musimy kopiować całej zawartości w każdej iteracji.

Na blogu już wspomniałem, że dzisiaj wyzwaniem dla programistów i producentów CPU to nie ich szybkość, ale pamięć. Pamięć stanowi element w architekturze komputerów, który może spowolnić działanie algorytmów. Budowanie szybszych CPU, nie ma aż tak dużego sensu, jeśli i tak będą musiały one czekać na dane. Co z tego, że operacja może być wykonana w kilka cykli, jeśli dostarczenie danych zajmuje kilkadziesiąt cykli.

Procesory mają wielowarstwową pamięć podręczną. Dostęp do L1 jest najszybszy i z tego względu najbardziej korzystny dla nas. Ponadto, czytając jakaś zmienną, pobieramy tak naprawdę np. 64B, a nie wyłącznie pojedynczą zmienną – pisałem o tym w przypadku false sharing.

Jeśli zależy nam na wysokiej wydajności, szczególnie w środowisku wielowątkowym, powinniśmy wziąć pod uwagę jak dane są rozmieszczone w pamięci. Ma to ogromne znaczenie na wspomniany false sharing, jak i po prostu na szybkość dostępu do danych.

Wracając do przykładu… Pojedynczy element listy, to Int32 czyli 32 bity (4 bajty). Jeśli cache line wynosi 64 bajty to w jednym odczycie możemy pobrać aż 16 elementów tablicy. Kolejne iteracje zatem będą korzystać wyłącznie z L1 co jest ogromną optymalizacją (ale może spowodować również false sharing – zawsze pamiętajmy o tym).

A co w przypadku LinekdList? Musimy najpierw zobaczyć, co zawiera LinkedListNode:

public sealed class LinkedListNode<T>
{
/// <summary>
/// Initializes a new instance of the <see cref="T:System.Collections.Generic.LinkedListNode`1"/> class, containing the specified value.
/// </summary>
/// <param name="value">The value to contain in the <see cref="T:System.Collections.Generic.LinkedListNode`1"/>.</param>
[TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
public LinkedListNode(T value);
/// <summary>
/// Gets the <see cref="T:System.Collections.Generic.LinkedList`1"/> that the <see cref="T:System.Collections.Generic.LinkedListNode`1"/> belongs to.
/// </summary>
/// 
/// <returns>
/// A reference to the <see cref="T:System.Collections.Generic.LinkedList`1"/> that the <see cref="T:System.Collections.Generic.LinkedListNode`1"/> belongs to, or null if the <see cref="T:System.Collections.Generic.LinkedListNode`1"/> is not linked.
/// </returns>
public LinkedList<T> List { [TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")] get; }
/// <summary>
/// Gets the next node in the <see cref="T:System.Collections.Generic.LinkedList`1"/>.
/// </summary>
/// 
/// <returns>
/// A reference to the next node in the <see cref="T:System.Collections.Generic.LinkedList`1"/>, or null if the current node is the last element (<see cref="P:System.Collections.Generic.LinkedList`1.Last"/>) of the <see cref="T:System.Collections.Generic.LinkedList`1"/>.
/// </returns>
public LinkedListNode<T> Next { get; }
/// <summary>
/// Gets the previous node in the <see cref="T:System.Collections.Generic.LinkedList`1"/>.
/// </summary>
/// 
/// <returns>
/// A reference to the previous node in the <see cref="T:System.Collections.Generic.LinkedList`1"/>, or null if the current node is the first element (<see cref="P:System.Collections.Generic.LinkedList`1.First"/>) of the <see cref="T:System.Collections.Generic.LinkedList`1"/>.
/// </returns>
public LinkedListNode<T> Previous { get; }
/// <summary>
/// Gets the value contained in the node.
/// </summary>
/// 
/// <returns>
/// The value contained in the node.
/// </returns>
public T Value { [TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")] get; [TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")] set; }
}

Widać wyraźnie, że oprócz wartości (Value), mamy wskaźnik na następny i poprzedni element oraz całą listę. Z tego względu, w pojedynczym cache-line, można załadować dużo mniej elementów, niż to ma miejsce w przypadku tablicy\listy.

Problemy z rozmieszczeniem elementów w pamięci są dość częste zatem należy zdawać sobie sprawę, jak to działa w przypadku poszczególnych struktur danych.

Tablicami wielowymiarowy zajmowałem  już się tutaj. Dla przypomnienia:

for (int row = 0; row < N; row++)
{
    for (int column = 0; column < N; column++)
    {
        array[row, column] = GetRandomValue();
    }
}

for (int column = 0; column < N; column++)
{
    for (int row = 0; row < N; row++)
    {
        array[row, column] = GetRandomValue();
    }
}

Jeśli nie jest jasne dlaczego jedna z powyższych pętli jest dużo wolniejsza, zachęcam do przeczytania ponownie wspomnianego wpisu.

Wydajność metod wirtualnych i niewirtualnych

Kiedyś pisałem o modyfikatorze sealed i dlaczego warto go używać jeśli chodzi o kwestie dobrych praktyk. Podobne mam zdanie co do modyfikatora virtual – używam wyłącznie jak mam takie wymagania. Zawsze zaczynam od najbardziej restrykcyjnych modyfikatorów. Klasy deklaruję jako sealed internal, a metody jako private. Nie zaznaczam metod jako virtual “na zapas”, ponieważ skoro nie są one zaprojektowane pod tym kątem to może przynieść to więcej kłopotów niż korzyści. Analogiczne zasady stosuje się np. w bezpieczeństwie – użytkownikowi zawsze nadaje się minimalny zestaw niezbędnych pozwoleń.

Dzisiaj jednak nie o dobrych praktykach a o wydajności. Bardzo możliwe, że niektóre metody zostaną skonwertowane do inline. Co to oznacza? Zamiast wywoływać daną metodę, po prostu jej ciało zostanie skopiowanie. Rozważmy następujący przykład:

private int Sum(int a,int b)
{
    return a+b;
}


private void Main()
{
    // jakas logika
    int c= Sum(a,b);
    // dalszy ciag logiki.
}

Skonwertowanie Sum do inline będzie wyglądało następująco:

private void Main()
{
    // jakas logika
    int c = a+b;
    // dalszy ciag logiki
}

Bardzo możliwe, że nawet a i b zostaną zastąpione konkretnymi wartościami jeśli to tylko możliwe. Bez metody inline, system operacyjny musi skoczyć do innej metody, wykonać ją a potem z powrotem powrócić w dane miejsce. Ze strony wydajnościowej jest to wiele wolniejsze (pamiętajmy o stosie, przekazywaniu parametrów) oraz może mieć konsekwencje jeśli chodzi o caching i wielowątkowość.

Metody wirtualne nigdy oczywiście nie mogą być inline co jest oczywiście bardzo naturalne. Napiszmy zatem program, który porówna taką samą metodę oznaczoną virtual i bez virtual:

class Sample
{
   public virtual int VirtualMethod(int a,int b)
   {
       return a + b;
   }

   public int NonVirtualMethod(int a, int b)
   {
       return a + b;
   }
}
internal class Test
{
   private const int Tests = 10;
   private const int n = 1000000000;

   private static void Main(string[] args)
   {
       TestVirtual();
       TestNonVirtual();
   }

   private static void TestVirtual()
   {
       Sample sample = new Sample();

       for (int i = 0; i < Tests; i++)
       {
           Stopwatch stopwatch = Stopwatch.StartNew();

           for (int j = 0; j < n; j++)
           {
               int c = sample.VirtualMethod(5, 10);
           }

           Console.WriteLine("Virtual: {0}", stopwatch.ElapsedMilliseconds);
       } 
   }
   private static void TestNonVirtual()
   {
       Sample sample = new Sample();

       for (int i = 0; i < Tests; i++)
       {
           Stopwatch stopwatch = Stopwatch.StartNew();

           for (int j = 0; j < n; j++)
           {
               int c = sample.NonVirtualMethod(5, 10);                    
           }
           Console.WriteLine("NonVirtual: {0}", stopwatch.ElapsedMilliseconds);
       }           
   }
}

W trybie DEBUG wyniki są następujące:

image

Nie powinno dziwić, że wyniki są bardzo podobne, ponieważ w DEBUG nie powinny być dokonywane optymalizacje.

Spróbujmy zatem przełączyć się do Release i wykonać analogiczny eksperyment:

image

Tutaj wyniki są już znaczące ponieważ niewirtualna wersja jest wywoływana w sposób inline.

Ostatnia rzecz, to porównanie metody niewirtualnej, która nie może zostać wywołana inline, z wirtualną. Wystarczy użyć atrybutu [MethodImpl(MethodImplOptions.NoInlining)], który zabrania wywołania danej metody w sposób inline:

class Sample
{
   public virtual int VirtualMethod(int a,int b)
   {
       return a + b;
   }
   [MethodImpl(MethodImplOptions.NoInlining)]
   public int NonVirtualMethod(int a, int b)
   {
       return a + b;
   }
}

Rezultat:

image

Różnica bardzo mało znacząca ponieważ zarówno wirtualne, jak i niewirtualne metody w .NET są wywoływane za pomocą instrukcji callvirt. Zdecydowano się na taki krok, ponieważ callvirt sprawdza, czy wskaźnik nie jest NULL. Z tego względu, wywołanie metody na wskaźniku, który wskazuje NULL spowoduje wyjątek NullReferenceException. Metody statyczne z kolei korzystają z innej instrukcji(call), która nie sprawdza wskaźnika NULL. Z tego względu, wywołanie metody rozszerzającej, która jest zawsze statyczna, na obiekcie NULL nie spowoduje żadnego wyjątku.

Code Review: Scalanie pętli za pomocą LINQ

Czasami można zaobserwować następujący kod:

int[] firstArray = Enumerable.Range(1, 1000).ToArray();
int[] secondArray = Enumerable.Range(1, 1000).ToArray();

foreach (int item in firstArray)
{
    Process(item);
}

foreach (int item in secondArray)
{
    Process(item);
}

Mam na myśli sytuacje kiedy mamy kilka osobnych tablic, ale przetwarzanie ich jest takie same lub bardzo podobne. Inny przykład to przetworzenie tablicy, a potem pojedynczego elementu pochodzącego z innego źródła:

int[] firstArray = Enumerable.Range(1, 1000).ToArray();
var singleItem=GetItem(...);

foreach (int item in firstArray)
{
 Process(item);
}

Process(singleItem);

W skrócie, w kodzie takim występujące kilka źródeł danych albo pojedyncze elementy.  Tworzenie kilku pętli, jak wyżej jest brzydkie i niewygodne. Lepiej skorzystać z LINQ i złączyć kilka źródeł w jedno. Możemy to zrobić za pomocą concat albo union:


int[] firstArray = Enumerable.Range(1, 1000).ToArray();
int[] secondArray = Enumerable.Range(1, 1000).ToArray();

foreach (int item in firstArray.Union(secondArray))
{
    Process(item);
}

foreach (int item in firstArray.Concat(secondArray))
{
    Process(item);
}

Jaka jest różnica między union a concat? Union złączy tylko unikalne elementy czyli to tak jakby wywołać concat a potem na końcu Distinct(), który zwraca wyłącznie unikalne liczby. Oczywiście ma to ogromny wpływ na wydajność ponieważ należy w przypadku union wywoływać Equals oraz GetHash:

private static void Main(string[] args)
{

  int[] firstArray = Enumerable.Range(1, n).ToArray();
  int[] secondArray = Enumerable.Range(1, n).ToArray();

  TestConcat(firstArray,secondArray);
  TestUnion(firstArray,secondArray);

}
private static void TestUnion(int[] array1, int[] array2)
{           
  for (int i = 0; i < Tests; i++)
  {
      Stopwatch stopwatch = Stopwatch.StartNew();

      foreach (var item in array1.Union(array2))
      {
      }

      Console.WriteLine("Union: {0}", stopwatch.ElapsedTicks);
  }
}

private static void TestConcat(int[] array1, int[] array2)
{           
  for (int i = 0; i < Tests; i++)
  {
      Stopwatch stopwatch = Stopwatch.StartNew();

      foreach (var item in array1.Concat(array2))
      {
      }

      Console.WriteLine("Concat: {0}",stopwatch.ElapsedTicks);
  }
}

Wynik:

image

Sprawa może wydawać się łatwa ale czasami, gdy kod jest bardziej skomplikowany nie jest zbyt oczywista. W praktyce, nie zawsze mamy dokładnie te same typy i wtedy należy dokonać np. transformacji aby ujednolicić przetwarzane dane.

Struktury oraz klasy– zużycie pamięci

W dzisiejszym wpisie, pokażę jaki wpływ mają klasy na zużycie pamięci. W ostatnim poście pokazałem korzyści płynące ze struktur jeśli mamy do czynienia z małymi kontenerami na dane. Najlepiej odpalmy po prostu następujący kod:

internal class Test
{
   const int n = 10000000;

   private static void Main(string[] args)
   {
       TestStruct();
       TestClass();
   }
   private static void TestStruct()
   {
       long current = GC.GetTotalMemory(true);

       var values = new PointValue[n];
       long after = GC.GetTotalMemory(true);

       Console.WriteLine("Struktury, potrzebna pamiec: {0}", after - current);
   }
   private static void TestClass()
   {
       long current = GC.GetTotalMemory(true);

       var values = new PointRef[n];
       for (int i = 0; i < values.Count(); i++)
           values[i] = new PointRef();

       long after = GC.GetTotalMemory(true);

       Console.WriteLine("Klasy, potrzebna pamiec: {0}",after - current);
   }
}

W programiku po prostu tworzymy i alokujemy tablicę struktur oraz klas. Wyniki są następujące:

image

Liczby tak naprawdę nie powinni zadziwiać – klasy tworzą masę dodatkowych pól co dla prostych typów nie jest przecież wykorzystywane. Oprócz tego warto zwrócić uwagę na kilka innych aspektów. Sama alokacja tablicy klas jest dość powolnym procesem ponieważ należy przejść przez taką tablicę i stworzyć każdy element osobno. W przypadku struktur jest to bardzo szybkie ponieważ wszystkie elementy tworzone są automatycznie od razu – stąd nie można definiować ręcznie domyślnych konstruktorów w strukturach.

Inną kwestią jest zwolnienie zasobów co jest naprawdę ogromną wadą klas. Proszę zauważyć, że GC ma dużo do roboty, gdy mamy wiele małych obiektów. W takiej sytuacji musimy przejść poprzez graf składający się z wielu węzłów. Im mniej obiektów, tym mniej poszukiwań i tym szybsza kolekcja.

Warto również zrobić analogiczny eksperyment, pokazujący czas alokacji oraz zwolnienia pamięci:

internal class Test
{
   const int n = 10000000;

   private static void Main(string[] args)
   {
       Stopwatch stopwatch = Stopwatch.StartNew();
       TestStruct();
       GC.Collect();
       GC.WaitForPendingFinalizers();
       Console.WriteLine("Alokacja oraz zwolnienie struktur: {0}",stopwatch.ElapsedTicks);

       stopwatch = Stopwatch.StartNew();
       TestClass();
       GC.Collect();
       GC.WaitForPendingFinalizers();
       Console.WriteLine("Alokacja oraz zwolnienie klas: {0}", stopwatch.ElapsedTicks);
   }
   [MethodImpl(MethodImplOptions.NoInlining)]
   private static void TestStruct()
   {
       var values = new PointValue[n];
   }
   [MethodImpl(MethodImplOptions.NoInlining)]
   private static void TestClass()
   {
       var values = new PointRef[n];
       for (int i = 0; i < values.Count(); i++)
           values[i] = new PointRef();
   }
}

Wynik:

image

Różnica jest znów ogromna, z powodów opisanych w ostatnich wpisach. Tablica struktur to jedna ciągła całość w pamięci, z kolei w przypadku klas są to wskaźniki do obiektów. Czas wykonania kolekcji zależy od liczby obiektów, przede wszystkim tych osiągalnych. Pamiętajmy, że algorytm Mark&Sweep musi najpierw zaznaczyć wszystkie osiągalne obiekty aby potem usunąć te nieosiągalne (niezaznaczone). Z tego wniosek, że szybciej jest przejść przez taki graf, gdzie większość obiektów jest nieosiągalna albo są to duże obiekty, których nie jest tak wiele w systemie.