O wydajności w aplikacjach .NET
16.02.2012 | aktual.: 16.02.2012 10:38
Coraz częściej użytkownicy vortalu dobreprogramy opisują ulubione narzędzia programistyczne i wykorzystywane do różnorakich celów języki programowania. Takie wpisy budzą całkiem spore zainteresowanie wśród czytelników dobrychprogramów. Niemniej czytając komentarze pod wybranymi wpisami odnosi się wrażenie nieustannej bitwy ideologicznej, której stronami są zwolennicy tradycyjnych rozwiązań tj. programowania natywnego z wykorzystaniem języków pokroju C/C++ oraz zwolennicy kodu zarządzanego przez maszyny wirtualne (np. JVM dla Javy, CLR dla C# i pozostałych).
Nierzadko podejmowaną kwestią w tych burzliwych dyskusjach jest wydajność tworzonych aplikacji w zależności od wybranej technologii. Ech... ta wydajność... termin określający niepotrzebną bolączkę programistów naszych czasów.
W codziennej pracy wykorzystuję kilka narzędzi programistycznych z .NET'em na czele. Zaryzykuję stwierdzenie, że w dobie komputerów o tak znaczących mocach obliczeniowych i doskonale zoptymalizowanych procesów kompilacji i wykonywania kodu zarządzanego aplikacje działające w ramach platformy .NET nie ustępują tradycyjnym rozwiązaniom. Przynajmniej w przeważającej większości przypadków zastosowań.
Musicie bowiem zgodzić się z tym, że - biorąc pod uwagę wszelkie udogodnienia wprowadzone w ramach kodu zarządzanego - responsywność oprogramowania mierzona subiektywnym odczuciem użytkownika programu nie różni się od tej, którą oferuje nam świat "niebezpiecznych wskaźników i wycieków pamięci" (celowo w cudzysłowie).
Jedno ale
Taki stan rzeczy uzyskamy tylko pod warunkiem bezwzględnego przestrzegania zasad, zaleceń czy też inaczej wzorców i praktyk określonych przez producenta danej platformy programistycznej - tu Microsoftu. Potocznie konkludując, jeśli bardziej niż treści oferowane przez MSDN cenisz niby-programistyczne podejście typu google-kopiuj-wklej-it works! to sorry, ale demonami szybkości twoje aplikacje nigdy nie będą, a i przekonań się złych nabawisz.
Studia, te wyższe i te niższe, z całą swą przebojowością mają z natury nie uczyć, lecz wskazać drogę właściwego korzystania ze źródeł, wszelakich, nie tylko tych określonych w podstawie programowej. Jako programiści popełniamy błędy wielokrotnie, ale błędem kardynalnym jest niechęć do korzystania z ogólnodostępnej bazy wiedzy oferowanej przez producenta lub uznawanie jej za zbędną.
Niniejszy tekst rozpoczyna cykl wpisów traktujących o wydajności w aplikacjach .NET. Bardzo skromnie i na przykładach postaram się przybliżyć wam wybrane, lecz z całą pewnością nie wszystkie, zagadnienia wpływające na wydajność kodu zarządzanego. Jeśli gdziekolwiek się pomylę, poprawcie mnie. A jeśli wykorzystujecie inne technologie (np. Java), spróbujcie przenieść stosowną część przedstawionych tu treści na swoje ulubione platformy i poinformujcie nas o swoich rezultatach.
Na wstępie rozważań warto by określić na czym, pod pojęciem wydajności, najbardziej nam zależy. Niech na potrzeby niniejszego cyklu wskaźnikiem tym będzie szybkość wykonywania kodu. Aby umożliwić wszystkim zainteresowanym wykonywanie podobnych pomiarów na niedotnetowych platformach działających pod kontrolą systemu Windows, jako podstawowe narzędzie pomiarowe wykorzystam funkcję QueryPerformanceCounter oraz QueryPerformanceFrequency dotępne w Win32 API. Dokładność QueryPerformanceCounter jest rzędu nanosekundy. Zbliżoną do niej funkcjonalność w środowisku .NET Framework 2.0 i wyżej zapewnia klasa StopWatch w przestrzeni nazw System.Diagnostics (ekhm, dokładnie taką samą).
W programowaniu unikam mieszania języków polskiego i angielskiego, dlatego trzymam się konwencji stosowania angielskich nazw dla klas, obiektów i innych reprezentujących kluczowe elementy kodu źródłowego.
Prosta implementacja QueryPermormanceCounter w kodzie zarządzanym (C#), czyli tzw. klasa opakowująca (prawda, że "wrapper" brzmi lepiej?), którą będziemy notorycznie wykorzystywali w przyszłości wygląda następująco:
using System; using System.Text; using System.ComponentModel; using System.Security; using System.Runtime.InteropServices; namespace TestPerformance1 { class Program { static void Main(string[] args) { PerformanceCounter counter = new PerformanceCounter(); counter.Start(); Console.WriteLine("Test wydajności...\n"); counter.Stop(); Console.WriteLine("Czas trwania testu:\n{0:F3} ns\n{1:F3} ms\n", counter.ElapsedNanoseconds, counter.ElapsedMiliseconds); } } class PerformanceCounter { [DllImport("kernel32.dll"), SuppressUnmanagedCodeSecurity] private static extern bool QueryPerformanceCounter(out long lpPerformanceCount); [DllImport("kernel32.dll")] private static extern bool QueryPerformanceFrequency(out long lpFrequency); private long start; private long stop; private long frequency; Decimal nanoMultiplier = new Decimal(1.0e9); Decimal miliMultiplier = new Decimal(1.0e3); public PerformanceCounter() { // Więcej informacji na: // http://msdn.microsoft.com/en-us/library/windows/desktop/ms644905(v=vs.85).aspx if (QueryPerformanceFrequency(out frequency) == false) throw new Win32Exception(); } public void Start() { QueryPerformanceCounter(out start); } public void Stop() { QueryPerformanceCounter(out stop); } public double ElapsedNanoseconds { get { return (((double)(stop - start) * (double)nanoMultiplier) / (double)frequency); } } public double ElapsedMiliseconds { get { return (((double)(stop - start) * (double)miliMultiplier) / (double)frequency); } } } }
Sprawdźcie czy działa, pobawcie się. Zwróćcie uwagę na atrybut SuppressUnmanagedCodeSecurity. Niebawem wykorzystamy naszą klasę w bardziej racjonalny sposób.