數據結構與算法系列(1)時間測試

時間測試 算法

——.NET數據結構與算法系列之一 數組

追憶20131112 數據結構

前言 框架

很久都把數據結構和算法的東西忘完了,最近想重溫下這些知識。所以就寫了<<數據結構與算法系列》的文章,但願能和你們共同窗習,共同探討這方面的知識。但願你們多多指異。 數據結構和算法

1.時間測試 性能

因爲本部份內容採用了一種實用的方法來分析數據結構與算法檢測,因此在這裏避開了使用大O分析法,而採用運行簡單基準測試的方法來代替。這種測試將會說明運行一段代碼須要多少秒數(或者不管什麼時間單位)。 學習

基準法測試是使用時間測試的方法來衡量運行完整算法所花費的時間長度。如同科學同樣,基準測試也像是一門藝術,並且爲了得到精準分析須要很當心測試代碼的方法。下面就來進行詳細討論。 測試

1.1一個簡單化的時間測試 ui

首先時間測試須要一此代碼。出於簡單的考慮,這裏將測試一個有關控制檯數組內容的子程序。代碼以下所示: spa

  1:         //創建數組
  2:         static void BuildArray(int[] arr)
  3:         {
  4:             for (int i = 0; i <= 99999; i++)
  5:             {
  6:                 arr[i] = i;
  7:             }
  8:         }
  9: 
 10:         //輸出數組
 11:         static void DisplayNums(int[] arr)
 12:         {
 13:             for (int i = 0; i <= arr.GetUpperBound(0); i++)
 14:             {
 15:                 Console.WriteLine(arr[i] + " ");
 16:             }
 17:         }

 

爲了測試這個子程序,須要建立一個就是,而且把子程序調用時的系統時間賦值給此變量。此外,還須要一個變量用來存儲子程序返回時的時間。根據這些內容寫出了下述這段代碼:

DateTime startTime;

TimeSpan endTime;

startTime = DateTime.Now; 
    endTime =
DateTime
.Now.Subtract(startTime);

個人機器配置:CPU 1.9GHz 四核,4G內存,Win7旗艦版,在上面運行代碼時,子程序的運行時間大約30秒左右(29.94)。雖然這段代碼對執行時間的測試好像挺有道理,可是在.NET環境下運行時間代碼是徹底不合適的。爲何呢?

1

首先,代碼測量的是從子程序調用開始到子程序返回主程序之間流失的時間。可是測試所測量的時間也包含了與C#程序同時運行的其餘進行所用的時間。

其次,時間代碼不考慮.NET環境下執行的無用單元收集。在類型.NET這樣的運行環境中,系統可能在執行無用單元收集的任何一個時間暫停。時間代碼實例沒有考慮無用單元收集時間,以及很容易受到無用單元收集影響的結果時間。那麼到底應該怎麼作呢?

1.2 用於.NET環境的時間測試

.NET 環境中須要考慮程序運行中的線程以及無用單元收集可能在任什麼時候候發生的事實。因此在編寫時間測試代碼時須要考慮這些狀況。

先來看一下如何處理無用單元收集。首先討論一下無用單元收集的用途。C#語言用有時被稱爲堆的內存來給參考類型(例如字符串、數組以及類事例對象)分配存儲空間。堆是用來保存數據項(前面提到的類型)的內存區域。諸如普通變量這樣的值類型則存儲在堆棧中。

引用的參考數據也存儲在堆棧中,可是實際的數據則是以參考類型的形式存儲在堆中。

當聲明變量的子程序徹底執行結束時就能夠釋放掉存儲在堆棧中的變量。而另外一方面,存儲在堆中的變量則會一直保留到調用無用單元收集進程的時候。當沒有引用堆數據的行爲時,只有經過無用單元收集才能夠移除這些數據。

在程序執行過程當中無用單元收集可能會發生在任什麼時候候。然而須要確保在實現時間測試代碼時沒有運行無用單元收集器。可是也許你們據說過經過強制調用無用單元收集器來進行專門

的無用單元收集。.NET 環境爲執行無用單元收集調用提供了專門的對象——GC。爲了使系統執行無用單元收集,能夠有以下簡單書寫:

GC.Collect();

可是不是全部都要這樣作的。存儲在堆中的每個對象都有一個稱爲finalizer 的專門方法。finalizer 方法是在刪除對象以前執行的最後一步。有關finalizer 方法的問題是,這些方法不是按照系統方式運行的。事實上,甚至沒法確信對象的finalizer 方法是否真的執行了,可是知道在肯定刪除對象以前須要執行此對象的finalizer 方法。爲了確信這一點,咱們添加了一行代碼來告訴程序等待堆上對象的全部finalizer 方法都運行後再繼續。此代碼行以下:

GC.WaitForPendingFinalizers();

已經清除了一個障礙,如今就剩下一個問題了——採用正確的線程。在.NET 環境中,程序運行在被稱爲應用程序域的進程中。這就容許操做系統在同一時間內分開運行每一個不一樣的程

序。在進程內,程序或程序的一部分是在線程內運行的。操做系統經過線程來分配程序的執行時間。在用時間測試程序代碼時,須要確信正在進行時間測試的代碼就在爲自身程序分配的進程中,而不在操做系統執行的其餘任務裏。在.NET 框架下經過使用Process 類能夠作到這一點。Process 類擁有的方法容許選取當前的進程、選取程序運行其內的線程,以及選取存儲線程開始執行時間的計時器。這些方法中的每個均可以合併成一個調用。此調用會把它的返回值賦值給一個變量用來存儲開始時間(TimeSpan 對象)。以下列代碼所示(沒錯,就是兩行代碼):

TimeSpan startingTime;

startingTime = Process.GetCurrentProcess().Threads[0].UserProcessorTime;

剩下要作的就是在進行時間測試的代碼段中止時捕獲時間。作法以下: 

TimeSpan duration;

duration = Process.GetCurrentProcess().Threads[0].UserProcessorTime.Subtract(startingTime);

如今把全部這些合併成一個程序。此程序的代碼和先前測試代碼是同樣的:

  1: using System;
  2: using System.Collections.Generic;
  3: using System.Linq;
  4: using System.Text;
  5: using System.Threading.Tasks;
  6: using System.Diagnostics;
  7: 
  8: namespace Chapter1
  9: {
 10:     static class NetTest
 11:     {
 12:         public static void Start()
 13:         {
 14:             //我的認爲這兩行代碼在如今的程序中不須要(程序的初始化以前與測試程序沒有關係)
 15:             //你們怎麼看 探討一下
 16:             GC.Collect();
 17:             GC.WaitForPendingFinalizers();
 18: 
 19:             TimeSpan startingTime;
 20:             startingTime = Process.GetCurrentProcess().Threads[0].UserProcessorTime;
 21: 
 22:             TimeSpan duration; ;
 23: 
 24:             int[] nums = new int[100000];
 25: 
 26:             BuildArray(nums);
 27: 
 28:             DisplayNums(nums);
 29:             DisplayNums(nums);
 30:             DisplayNums(nums);
 31: 
 32:             duration = Process.GetCurrentProcess().Threads[0].UserProcessorTime.Subtract(startingTime);
 33:             Console.WriteLine("Time:" + duration.TotalSeconds);
 34:         }
 35: 
 36:         //創建數組
 37:         static void BuildArray(int[] arr)
 38:         {
 39:             for (int i = 0; i <= 99999; i++)
 40:             {
 41:                 arr[i] = i;
 42:             }
 43:         }
 44: 
 45:         //輸出數組
 46:         static void DisplayNums(int[] arr)
 47:         {
 48:             for (int i = 0; i <= arr.GetUpperBound(0); i++)
 49:             {
 50:                 Console.WriteLine(arr[i] + " ");
 51:             }
 52:         }
 53:     }
 54: }
 55: 
調用便可:NetTest.Start(); 
2

採用新改進的時間測試代碼後,程序的返回值爲2.99。把此數值與先前初版時間測試代碼返回的將近30秒的數值進行比較。很明顯,這兩種時間測試方法之間存在顯著差別。於是.NET 環境中的時間測試代碼應該使用.NET 方法來作。

 

1.3 Timing Test

 

雖然不須要一個類來運行時間測試代碼,可是把代碼做爲類來重寫是有意義的,主要緣由是若是可以減小測試的代碼行數量,就可以保證代碼的清晰。

 

Timing 類須要下列數據成員:

1.startingTime——用來存儲正在測試的代碼的開始時間。

2.duration——用來存儲正在測試的代碼的終止時間。

straingTime duration 這兩個成員用來存儲時間,並且爲這些數據成員選擇使用TimeSpan數據類型。這裏就採用一種構造器方法,此默認構造器會把數據成員所有置爲0

正如看到的那樣,Timing 類是很小的,它只須要少許方法。下面是定義:

 

  1: using System;
  2: using System.Collections.Generic;
  3: using System.Linq;
  4: using System.Text;
  5: using System.Threading.Tasks;
  6: using System.Diagnostics;
  7: 
  8: namespace Chapter1
  9: {
 10:     class Timing
 11:     {
 12:         //用來存儲正在測試的代碼的開始時間
 13:         TimeSpan startingTime;
 14:         //用來存儲正在測試的代碼的終止時間
 15:         TimeSpan duration;
 16: 
 17:         public Timing()
 18:         {
 19:             startingTime = new TimeSpan(0);
 20:             duration = new TimeSpan(0);
 21:         }
 22: 
 23:         public void StopTime()
 24:         {
 25:             duration = Process.GetCurrentProcess().Threads[0].UserProcessorTime.Subtract(startingTime);
 26:         }
 27: 
 28:         public void StartTime()
 29:         {
 30:             GC.Collect();
 31:             GC.WaitForPendingFinalizers();
 32:             startingTime = Process.GetCurrentProcess().Threads[0].UserProcessorTime;
 33:         }
 34: 
 35:         public TimeSpan Result()
 36:         {
 37:             return duration;
 38:         }
 39:     }
 40: }
 41: 

這是用Timing 類改寫的用來測試DisplayNums 子程序的程序:

  1: using System;
  2: using System.Collections.Generic;
  3: using System.Linq;
  4: using System.Text;
  5: using System.Threading.Tasks;
  6: using System.Diagnostics;
  7: using System.Threading;
  8: 
  9: namespace Chapter1
 10: {
 11:     //這是用Timing 類改寫的用來測試DisplayNums 子程序的程序
 12:     class NetTestUseTiming
 13:     {
 14:         public void Start()
 15:         {
 16:             int[] nums = new int[100000];
 17: 
 18:             BuildArray(nums);
 19: 
 20:             Timing tObj = new Timing();
 21:             tObj.StartTime();
 22:             DisplayNums(nums);
 23:             DisplayNums(nums);
 24:             DisplayNums(nums);
 25: 
 26:             tObj.StopTime();
 27:             Console.WriteLine("time (.NET): " + tObj.Result().TotalSeconds);
 28:         }
 29: 
 30:         //創建數組
 31:         static void BuildArray(int[] arr)
 32:         {
 33:             for (int i = 0; i < 100000; i++)
 34:             {
 35:                 arr[i] = i;
 36:             }
 37:         }
 38: 
 39:         //輸出數組
 40:         static void DisplayNums(int[] arr)
 41:         {
 42:             for (int i = 0; i <= arr.GetUpperBound(0); i++)
 43:             {
 44:                 Console.WriteLine(arr[i] + " ");
 45:             }
 46:         }
 47:     }
 48: }
 49: 

經過把時間測試代碼移動到類裏的方法,這裏把主程序的代碼行數從減小。顯然這樣不會從程序中砍掉大量的代碼,而比砍掉代碼更重要的則是下降了主程序的複雜度。

若是沒有類,那麼把開始時間賦值給變量的操做就會像下面這樣:

startTime = Process.GetCurrentProcess( ).Threads[0].UserProcessorTime;

而若是使用Timing 類,那麼把開始時間賦值給類數據成員的方式以下所示:

tObj.startTime( );

經過把冗長的賦值語句封裝到類方法內,可使得代碼更便於閱讀並且出錯的可能更小了。

 

小結

上面的例子中儘管不須要編寫整個程序,可是一些程序的代碼以及要討論的庫都採用面向對象的方式來編寫。

Timing 類提供了簡單有效的方法來衡量所要學習的數據結構與算法的性能。

 

天氣仍是挺冷的,手腳冰涼,趕忙睡啦。必定要把這個系列堅持寫完。

   

    源程序下載:DataStructAndAlgorithm.zip 

參考書箱:<<數據結構與算法>>

相關文章
相關標籤/搜索