Blog (22)
Komentarze (619)
Recenzje (0)

O wydajności w aplikacjach .NET cz. 3

@alucosoftwareO wydajności w aplikacjach .NET cz. 325.02.2012 18:02

W poprzednim odcinku cyklu O wydajności w aplikacjach .NET omawialiśmy mechanizm Opakowywania i Rozpakowywania (ang. boxing/unboxing), czyli przekształcania pewnych typów danych w typ bazowy (tu Object) i z powrotem. Dziś zajmiemy się kwestią bardziej przyziemną, doskonale znaną wszystkim programistom... Ale czy na pewno?

Operacje na ciągach znaków

Środowisko .NET Framework udostępnia nam typ danych System.String na potrzeby reprezentowania ciągów znaków. Klasa String zawiera chyba wszystko "co tygrysy lubią najbardziej" i jest naprawdę obszernie i dokładnie opisana na stronach MSDN. Taki stan rzeczy wynika z prostego faktu - nasze całe życie opiera się na słowie pisanym i reprezentowaniu znaków w dowolnym języku naturalnym. W wirtualnym świecie słowa i znaki także odgrywają wiodącą rolę. Toteż decydując się na wybór języka programowania (a jest ich kilka) wielu z nas swą decyzję opiera na dostępności, w ramach danej platformy programistycznej, bogatej gamy metod umożliwiających przetwarzanie zarówno pojedynczych znaków jak i ich dłuższych odpowiedników.

Platforma .NET wprowadza znaczny poziom abstrakcji. Nie wszystko co wydaje się z pozoru proste - okiem początkującego programisty - musi mieć równie prostą implementację "za kulisami dotnetu". Obiekt klasy String w kodzie zarządzanym to nic innego jak pewna kolekcja obiektów klasy System.Char reprezentujących dany łańcuch znaków. Z chwilą utworzenia instancji (czyt. utworzenia obiektu) klasy String, wartość tego obiektu nie będzie mogła już ulec zmianie, jest niezmienna (ang. immutable)!.

Przeczytaj ponownie poprzednie zdanie

Wartości obiektów klasy String są wyłącznie do odczytu. Właśnie ta cecha obiektów reprezentujących ciągi znaków i jej powszechna nieznajomość jest powodem poważnych strat w wydajności kodu zarządzanego. Ale każdy popełnia błędy, dlatego spróbujmy czym prędzej naprawić własne.

  • Każdorazowa operacja na obiekcie klasy String powodująca zmianę wartości przechowywanego w nim łańcucha znaków sprawia, że: pamięć, w której przechowywany jest obecny ciąg znaków (wartość obiektu), zostanie porzucona
  • konieczna będzie alokacja nowego bloku pamięci zdolnego przechować nową wartość obiektu
  • ponieważ typ String jest typem referencyjnym, a wartości obiektów typu referencyjnego przechowywane są na stercie, stare, nieużywane już łańcuchy znaków będą podlegały - z czasem - odśmiecaniu (ang. garbage collection)

Analogicznie do poprzednich rozważań, pojedyncza operacja tego rodzaju nam nie zaszkodzi (z wyjątkiem naprawdę długich ciągów). W przypadkach, w których zarówno same ciągi znaków jak również ich ilość jest niewielka i z góry znana, śmiało wykorzystuj operator + w celu ich łączenia. Tu wykonamy tę operację wielokrotnie, w pętli, przy użyciu "standardowych praktyk" jak i zoptymalizowanego rozwiązania.


using System;
using System.Text;
using System.ComponentModel;
using System.Security;
using System.Runtime.InteropServices;

namespace TestPerformance3
{
    class Program
    {
        static void Main(string[] args)
        {
            // Implementacja klasy PerformanceCounter dostępna w pierwszej części cyklu
            PerformanceCounter counter = new PerformanceCounter();

            // Ilość iteracji ograniczona. Nie chcemy przecież wyjątku OutOfMemoryException
            int iterations = 100000;
            string str = "tekst";
            string result = "";

            // Domyślna pojemność początkowa
            StringBuilder sbResult = new StringBuilder();

            // Pojemność StringBuilder jest ograniczona, jednak w pętlach nie będziemy sprawdzali przekroczenia zakresu
            StringBuilder sbCapacityResult = new StringBuilder(iterations * str.Length <= Int32.MaxValue ? iterations * str.Length : Int32.MaxValue);

            counter.Start();
            for (int i = 0; i < iterations; i++)
            {
                result += str;
            }
            counter.Stop();
            Console.WriteLine("Operator +:");
            // Każda kolejna iteracja była bardziej kosztowna od poprzedniej
            Console.WriteLine("{0:F3} ns - pojedyncza iteracja", counter.ElapsedNanoseconds / iterations);
            Console.WriteLine("{0:F3} ms - całość", counter.ElapsedMiliseconds);

            Console.WriteLine();

            counter.Start();
            for (int i = 0; i < iterations; i++)
            {
                sbResult.Append(str);
            }
            counter.Stop();
            Console.WriteLine("StringBuilder.Append:");
            Console.WriteLine("StringBuilder.Capacity = {0}", sbResult.Capacity);
            Console.WriteLine("{0:F3} ns - pojedyncza iteracja", counter.ElapsedNanoseconds / iterations);
            Console.WriteLine("{0:F3} ms - całość", counter.ElapsedMiliseconds);

            Console.WriteLine();

            counter.Start();
            for (int i = 0; i < iterations; i++)
            {
                sbCapacityResult.Append(str);
            }
            counter.Stop();
            Console.WriteLine("StringBuilder.Append, wersja 2:");
            Console.WriteLine("StringBuilder.Capacity = {0}", sbCapacityResult.Capacity);
            Console.WriteLine("{0:F3} ns - pojedyncza iteracja", counter.ElapsedNanoseconds / iterations);
            Console.WriteLine("{0:F3} ms - całość", counter.ElapsedMiliseconds);

            Console.ReadKey();
        }
    }
}

Dla wielu, następujących po sobie konkatenacji (piękne słowo) lepszym rozwiązaniem jest wykorzystanie obiektu klasy StringBuilder, który traktować możemy jak akumulator (czyt. bufor).

Domyślna pojemność instancji StringBuilder to 16 znaków a jej przekroczenie skutkować będzie powołaniem do życia nowego obiektu będącego w stanie pomieścić nowy łańcuch tekstowy (dawniej pojemność wzrastała dwukrotnie). Tym samym oszczędzimy na wielu, dla nas niewidocznych, choć zupełnie niepotrzebnych operacjach związanych ze zwalnianiem, odśmiecaniem i przydzielaniem zasobów. StringBuilder nie jest typem pochodnym klasy String, lecz jak każdy inny dziedziczy po typie bazowym metodę ToString, którą w dowolnej chwili możemy wykorzystać w celu przekształcenia typu StringBuilder na typ String. Pamiętajmy, że im początkowa pojemność (ustalana w wywołaniu konstruktora klasy) będzie bliższa rzeczywiście wykorzystywanej w środowisku produkcyjnym - tym więcej zyskamy. Dokonać tego możemy w dwojaki sposób, bądź jawnie definiując zakres i wielkość wprowadzanych danych bądź mierząc średnie zużycie zasobów z wykorzystaniem narzędzi typu Profiler, ale o tym, miejmy nadzieję, wspomnę w przyszłości. Powodzenia!

P.S. Tym razem zaprezentuję wyniki.

Operator +: 231736,764 ns - pojedyncza iteracja 23173,676 ms - caˆłość

StringBuilder.Append: StringBuilder.Capacity = 504192 12,695 ns - pojedyncza iteracja 1,269 ms - caˆłość

StringBuilder.Append, wersja 2: StringBuilder.Capacity = 500000 12,728 ns - pojedyncza iteracja 1,273 ms - caˆłość

Szanowna Użytkowniczko! Szanowny Użytkowniku!
×
Aby dalej móc dostarczać coraz lepsze materiały redakcyjne i udostępniać coraz lepsze usługi, potrzebujemy zgody na dopasowanie treści marketingowych do Twojego zachowania. Twoje dane są u nas bezpieczne, a zgodę możesz wycofać w każdej chwili na podstronie polityka prywatności.

Kliknij "PRZECHODZĘ DO SERWISU" lub na symbol "X" w górnym rogu tej planszy, jeżeli zgadzasz się na przetwarzanie przez Wirtualną Polskę i naszych Zaufanych Partnerów Twoich danych osobowych, zbieranych w ramach korzystania przez Ciebie z usług, portali i serwisów internetowych Wirtualnej Polski (w tym danych zapisywanych w plikach cookies) w celach marketingowych realizowanych na zlecenie naszych Zaufanych Partnerów. Jeśli nie zgadzasz się na przetwarzanie Twoich danych osobowych skorzystaj z ustawień w polityce prywatności. Zgoda jest dobrowolna i możesz ją w dowolnym momencie wycofać zmieniając ustawienia w polityce prywatności (w której znajdziesz odpowiedzi na wszystkie pytania związane z przetwarzaniem Twoich danych osobowych).

Od 25 maja 2018 roku obowiązuje Rozporządzenie Parlamentu Europejskiego i Rady (UE) 2016/679 (określane jako "RODO"). W związku z tym chcielibyśmy poinformować o przetwarzaniu Twoich danych oraz zasadach, na jakich odbywa się to po dniu 25 maja 2018 roku.

Kto będzie administratorem Twoich danych?

Administratorami Twoich danych będzie Wirtualna Polska Media Spółka Akcyjna z siedzibą w Warszawie, oraz pozostałe spółki z grupy Wirtualna Polska, jak również nasi Zaufani Partnerzy, z którymi stale współpracujemy. Szczegółowe informacje dotyczące administratorów znajdują się w polityce prywatności.

O jakich danych mówimy?

Chodzi o dane osobowe, które są zbierane w ramach korzystania przez Ciebie z naszych usług, portali i serwisów internetowych udostępnianych przez Wirtualną Polskę, w tym zapisywanych w plikach cookies, które są instalowane na naszych stronach przez Wirtualną Polskę oraz naszych Zaufanych Partnerów.

Dlaczego chcemy przetwarzać Twoje dane?

Przetwarzamy je dostarczać coraz lepsze materiały redakcyjne, dopasować ich tematykę do Twoich zainteresowań, tworzyć portale i serwisy internetowe, z których będziesz korzystać z przyjemnością, zapewniać większe bezpieczeństwo usług, udoskonalać nasze usługi i maksymalnie dopasować je do Twoich zainteresowań, pokazywać reklamy dopasowane do Twoich potrzeb. Szczegółowe informacje dotyczące celów przetwarzania Twoich danych znajdują się w polityce prywatności.

Komu możemy przekazać dane?

Twoje dane możemy przekazywać podmiotom przetwarzającym je na nasze zlecenie oraz podmiotom uprawnionym do uzyskania danych na podstawie obowiązującego prawa – oczywiście tylko, gdy wystąpią z żądaniem w oparciu o stosowną podstawę prawną.

Jakie masz prawa w stosunku do Twoich danych?

Masz prawo żądania dostępu, sprostowania, usunięcia lub ograniczenia przetwarzania danych. Możesz wycofać zgodę na przetwarzanie, zgłosić sprzeciw oraz skorzystać z innych praw wymienionych szczegółowo w polityce prywatności.

Jakie są podstawy prawne przetwarzania Twoich danych?

Podstawą prawną przetwarzania Twoich danych w celu świadczenia usług jest niezbędność do wykonania umów o ich świadczenie (tymi umowami są zazwyczaj regulaminy). Podstawą prawną przetwarzania danych w celu pomiarów statystycznych i marketingu własnego administratorów jest tzw. uzasadniony interes administratora. Przetwarzanie Twoich danych w celach marketingowych realizowanych przez Wirtualną Polskę na zlecenie Zaufanych Partnerów i bezpośrednio przez Zaufanych Partnerów będzie odbywać się na podstawie Twojej dobrowolnej zgody.