Costurile concurenței: aspecte practice de evaluare (Visual Studio)
Aug 2nd, 2012 | By Dumitru | Category: Paralelism și concurențăAplicarea concurenței per ansamblu aduce beneficii de performanță. Dar organizarea interacțiunilor concurente implică și costuri, care necesită evaluate întru stabilirea unei strategii potrivite de dezvoltare a aplicației.
Utilizarea instrumentarului facilitează evaluarea performanței, deci acestea trebuie să fie indispensabile procesului de dezvoltare. Prin urmare este firesc ca Visual Studio să includă un instrument de profiling, care oferă un larg spectru de parametri pentru analiza concurenței, numit Concurrency Visualizer [1], în care analiza aplicațiilor este orientată pe trei direcții esențiale: utilizare CPU, nuclee și fire de execuție (figura 1). Ultima categorie este destinată pentru descrieri ale entităților active de sistem în prisma execuției, sincronizărilor, operațiilor I/E, suspendărilor, dar și inter-dependențelor.
Figura 1. Vederile instrumentului de profiling Concurrency Visualizer (Visual Studio)
Rapoartele generate de către instrument permit analiza generală de sistem, dar și focusarea acesteia pe elemente cheie ce determină esențial costul concurenței. În acest context se menționează timpul de blocare a funcției, ce include și timpul de suspendare a funcțiilor pe care le apelează (figura 2).
Figura 2. Detalii despre apelul unei funcții în Concurrency Visualizer (Visual Studio)
Un alt parametru de analiză este numărul de suspendări ale unei funcții, găsit în rapoartele instrumentului prin termenul de contențiune (contention – eng.). Contențiunile și timpurile de blocare ale funcțiilor, în mod evident, reies din prezența concurenței în execuția aplicației. Analiza contențiunilor și timpurile de blocare permit deducerea problemelor din sistemul informatic elaborat, prin găsirea punctelor vulnerabile. Acestea pot provoca congestii în execuție, deci trebuie eliminate sau diminuate, începând prin a explica prezența acestora. De exemplu, în analiza problemei Cititori-Scriitori prezentată în figura 3, atrage atenția timpul de blocare aproape dublu a metodei WriterEvents.VisitRole(…), în comparație cu ReaderEvents.VisitRole(…), deși au succesiuni asemănătoare de evenimente. Acesta se explică prin faptul că pentru a asigura unicitatea instanței de rol de tip Writer se utilizează în prima metodă semaforul, care implică un timp de blocare suplementar determinat de funcția WaitHandle.WaitOne(). Astfel este facil de înțeles că acest timp de blocare poate deveni mai mare în condițiile când numărul de instanțe ale scriitorilor va crește.
Figura 3. Vederea Functions în Concurrency Visualizer (Visual Studio)
Prin urmare trebuie de menționat următoarele: costurile de concurență sunt determinate atât de platforma tehnologică, ce impune politicile de planificare a execuției proceselor și firelor, cât și de construcțiile de limbaj utilizate în programare. Astfel dacă timpul de blocare este determinat, în principal, de planificare și nedeterminismul scenariului executat, atunci costul direct al concurenței este determinat de timpul de utilizare a CPU de către instrucțiunile ce asigură concurența. Acest timp poate fi extras utilizând un alt instrument de profiling inclus în Visual Studio, numit Instrumentation, care oferă detalii ale apelurilor de funcții (figura 4).
Figura 4. Vederea Functions în Instrumentation (Visual Studio)
Aspectul care se dorește a fi evidențiat în raportul prezentat în figura 4 este că pe lângă timpul scurs (elapsed time), ce exprimă timpul total de „procesare” a funcției, incluzând orice timp de așteptare, este prezentat și timpul de utilizare CPU (application time). Acești importanți parametri sunt obținuți prin procedura numită instrumentare, care inserează cod suplimentar de preluare probe în timpul derulării aplicației. În consecință, deși se obțin date detaliate despre execuție, probele introduc un anumit grad de inexactitate. Overhead-ul acestui tip de instrumente este o problemă recunoscută [2], astfel că multe din ele, inclusiv instrumentele de profiling de la Microsoft, încearcă să compenseze prin deducerea inexactității din rezultate.
În încercarea de a confirma cele menționate, în cadrul cercetărilor au fost efectuate experimente de evaluare a costurilor temporale. Astfel rezultatele experimentale (efectuate pe aceeași platformă) au arătat că timpurile medii de utilizare CPU pentru instrucțiunile de sincronizare sunt mai mici decât cele prezentate de instrumentarul Microsoft.
Aplicația de evaluare se bazează pe exemplul prezentat în [3], care la rândul ei presupune utilizarea claselor ExecutionStopwatch (care determină timpul efectiv de utilizare a CPU pentru efectuarea setului de instrucțiuni supus evaluărilor [4]) și Stopwatch (inclusă în cadrul de programare .Net și care determină timpul total de execuție, incluzând timpul de așteptare [5]). Astfel măsurarea timpului necesar pentru lansarea unui fir de execuție arată după cum urmează:
int LoopLength = 10000;
using (ConcurrencyPerformance.Mesure())
{
for (int i = 0; i < LoopLength; ++i)
{
new Thread(Program.EmptyMethod).Start();
}
}
Clasa ConcurrencyPerformance este de tip IDisposable, aceasta permite oprirea „cronometrelor” la eliminarea instanței. Numărul mare de iterații în ciclul de măsurare este necesar pentru a face timpul suficient de mare pentru a putea fi măsurat. Deoarece măsurările pot fi influențate de alte activități din sistem, sunt necesare evaluări repetate, ale căror variații sunt nivelate prin calculul mediei aritmetice ale rezultatelor obținute. Iar prin calculul coeficientului de variație (raportul dintre abaterea medie pătratică și media aritmetică) se justifică reprezentativitatea mediei aritmetice (Tabelul 1).
Tabelul 1 – Rezultatele măsurărilor experimentale ale utilizării CPU (platforma x86/.Net 4)
Bloc instrucțiuni | Încercări | Iterații | Media aritmetică per bloc instrucțiuni |
Abaterea medie pătratică | Coeficientul de variație |
new Thread(…).Start();
|
20 | 10 mii | 244,8 μs | 30 μs | 0,12 |
Task task = Task.Factory.StartNew(…);
task.Wait();
|
20 | 1 mln. | 4,3 μs | 0,4 μs | 0,09 |
lock (lockObject)
{
Monitor.Wait(lockObject);
Monitor.PulseAll(lockObject);
}
|
20 | 1 mln. | 3,8 μs | 0,01 μs | 0,003 |
semaphore.WaitOne();
semaphore.Release();
|
20 | 1 mln. | 3,1 μs | 0,4 μs | 0,13 |
countdownEvent.Signal();
countdownEvent.Reset();
|
20 | 10 mln. | 0,13 μs | 0,018 μs | 0,14 |
barrier.AddParticipants();
barrier.SignalAndWait();
barrier.RemoveParticipant();
|
20 | 1 mln. | 0,51 μs | 0,066 μs | 0,13 |
Referințe
1. Shafi, Hazim. Performance Tuning with the Concurrency Visualizer in Visual Studio 2010. MSDN Magazine. [Interactive]. s.l. : Microsoft Corporation, Martie 2010. http://msdn.microsoft.com/en-us/magazine/ee336027.aspx.
2. Farrell, Chris. The Fast Guide to Application Profiling. Simple-Talk. [Interactiv] 22 Aprilie 2010. [Citat: 28 Iulie 2012.] http://www.simple-talk.com/content/article.aspx?article=1011.
3. Berezovskiy, Sergey. How to measure performance of a managed thread in .NET. Stack Overflow. [Online] Ianuarie 20, 2011. [Cited: August 1, 2012.] Answers. http://stackoverflow.com/a/4746146.
4. Chen, Liran. ExecutionStopwatch. The Code Project. [Interactiv]. Noiembrie 22, 2008. http://www.codeproject.com/Articles/31152/ExecutionStopwatch.
5. Microsoft Corporation. Stopwatch Class. MSDN Library. [Interactiv] 2010. [Citat: 1 August 2012.] http://msdn.microsoft.com/en-us/library/system.diagnostics.stopwatch.aspx.