عملکرد کلاس Stopwatch در C# سی شارپ

هر برنامه نویسی که با کلاس Stopwatch کار کرده باشد احتمالا به این موضوع پی برده است که اندازه گیری های این کلاس در یک کامپیوتر و طی عملیات های مشابه می تواند بین 25 الی 30 درصد در Run Time متفاوت باشد. به کمک الگوریتمی که امروز به کار با آن خواهیم پرداخت می توانیم علت این را با هم مشاهده کنیم و راهکار های حل آنرا بیاموزیم.
امروزه CPU های مدرن دارای هسته های چندگانه، کش های حجیم و و قسمت های پردازش مختلفی هستند که همه ی اینها در تست یک الگوریتم اثرات خود را (هر چند کوچک) می گذارند. از این رو ما در تست این الگوریتم از روش تست بلک باکس (run time measurement) استفاده میکنیم تا هر چه بیشتر به عملکرد واقعی الگوریتم خود نزدیک شویم.

قبل از شروع به کار باید از عدم سوئیچینگ و تعویض هسته های CPU اطمینان حاصل کنیم. زیرا همین مسئله می تواند باعث رد شدن کش شود و تاثیر بسیار عمده ای در نتیجه آزمایش و تست ما بگذارد. برای محدود کردن هسته پردازشگر، ProcessorAffinity می تواند به کمک ما بیاید:

   
Process.GetCurrentProcess().ProcessorAffinity = new IntPtr(2); // تنها استفاده از هسته دوم

برای منحصر به فرد سازی بیشتر CPU، باید کاری کنیم که سایر موضوعات و قسمت ها از این هسته استفاده نکنند.   

Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;
Thread.CurrentThread.Priority = ThreadPriority.Highest;

همچنین برای کاهش خطا هنگام شروع کار و پردازش اولیه، به اصطلاح به گرم کردن سیستم و آماده کردن آن برای محاسبات می پردازیم (warmup phase). معمولا زمان مورد نیاز برای این مرحله 1000 الی 1500 میلی ثانیه است. اکنون با کد زیر 1200 میلی ثانیه به کد ورزشکار خود اجازه گرم کردن قبل از گرفتن تست را می دهیم!!  

stopwatch.Start();
 while (stopwatch.ElapsedMilliseconds < 1200)
 {
 result = TestFunction(seed, count);
 }
 stopwatch.Stop();

و اما نمونه کاملی از الگوریتم:

using System;
 using System.Diagnostics;
 using System.Threading;
 
namespace PreciseMeasure
 {
 class Program
 {
 static void Main(string[] args)
 {
 Stopwatch stopwatch = new Stopwatch();
 
long seed = Environment.TickCount; // Prevents the JIT Compiler
 // from optimizing Fkt calls away
 long result = 0;
 int count = 100000000;
 
Console.WriteLine("20 Tests without correct preparation");
 Console.WriteLine("Warmup");
 for (int repeat = 0; repeat < 20; ++repeat)
 {
 stopwatch.Reset();
 stopwatch.Start();
 result ^= TestFunction(seed, count);
 stopwatch.Stop();
 Console.WriteLine("Ticks: " + stopwatch.ElapsedTicks +
 " mS: " +stopwatch.ElapsedMilliseconds);
 }
 
Process.GetCurrentProcess().ProcessorAffinity =
 new IntPtr(2); // Uses the second Core or Processor for the Test
 Process.GetCurrentProcess().PriorityClass =
 ProcessPriorityClass.High; // Prevents "Normal" processes
 // from interrupting Threads
 Thread.CurrentThread.Priority =
 ThreadPriority.Highest; // Prevents "Normal" Threads
 // from interrupting this thread
 
Console.WriteLine();
 Console.WriteLine();
 Console.WriteLine("20 Tests with correct preparation");
 Console.WriteLine("Warmup");
 stopwatch.Reset();
 stopwatch.Start();
 while (stopwatch.ElapsedMilliseconds < 1200) // A Warmup of 1000-1500 mS
 // stabilizes the CPU cache and pipeline.
 {
 result = TestFunction(seed, count); // Warmup
 }
 stopwatch.Stop();
 
for (int repeat = 0; repeat < 20; ++repeat)
 {
 stopwatch.Reset();
 stopwatch.Start();
 result ^= TestFunction(seed, count);
 stopwatch.Stop();
 Console.WriteLine("Ticks: " + stopwatch.ElapsedTicks +
 " mS: " + stopwatch.ElapsedMilliseconds);
 }
 Console.WriteLine(result); // prevents optimizations (current compilers are
 // too silly to analyze the dataflow that deep, but we never know )
 }
 
public static long TestFunction(long seed, int count)
 {
 long result = seed;
 for (int i = 0; i < count; ++i)
 {
 result ^= i ^ seed; // Some useless bit operations
 }
 return result;
 }
 }
 }

نتیجه تست:
بدون آماده سازی سیستم جهت تست:

Ticks: 1580367 mS: 572
Ticks: 1577003 mS: 571
Ticks: 1576140 mS: 571
Ticks: 1560964 mS: 565
Ticks: 1351663 mS: 489
Ticks: 1248383 mS: 452
Ticks: 1115361 mS: 404
Ticks: 1112813 mS: 403
Ticks: 1113112 mS: 403
Ticks: 1112012 mS: 402
Ticks: 1330444 mS: 482
Ticks: 1558493 mS: 564
Ticks: 1501097 mS: 543
Ticks: 1517796 mS: 549
Ticks: 1542712 mS: 558
Ticks: 1574959 mS: 570
Ticks: 1483975 mS: 537
Ticks: 1390578 mS: 503
Ticks: 1546904 mS: 560
Ticks: 1349507 mS: 488

آماده سازی سیستم جهت تست:

Ticks: 1110518 mS: 402
Ticks: 1110540 mS: 402
Ticks: 1110543 mS: 402
Ticks: 1110684 mS: 402
Ticks: 1110508 mS: 402
Ticks: 1110553 mS: 402
Ticks: 1110600 mS: 402
Ticks: 1110433 mS: 402
Ticks: 1110509 mS: 402
Ticks: 1110508 mS: 402
Ticks: 1110489 mS: 402
Ticks: 1110568 mS: 402
Ticks: 1110503 mS: 402
Ticks: 1110566 mS: 402
Ticks: 1110625 mS: 402
Ticks: 1110474 mS: 402
Ticks: 1110571 mS: 402
Ticks: 1110448 mS: 402
Ticks: 1110555 mS: 402
Ticks: 1110495 mS: 402

نتیجه گیری کلی: همیشه به یاد داشته باشیم که بهترین (کمترین) مقدار بدون آماده سازی به خوبی بدترین (بیشترین) مقدار با آماده سازی نیست. فراموش نکنیم که تغییرات هسته های CPU می تواند بیشترین تاثیر منفی ممکن را در نتیجه ی آزمایش های ما داشته باشند.