数据结构与算法系列(1)时间测试
时间测试
——.NET数据结构与算法系列之一
追忆,2013年11月12日
前言
好久都把数据结构和算法的东西忘完了,最近想重温下这些知识。因此就写了<<数据结构与算法系列》的文章,希望能和大家共同学习,共同探讨这方面的知识。希望大家多多指异。
1.时间测试
由于本部分内容采用了一种实用的方法来分析数据结构与算法检测,所以在这里避开了使用大O分析法,而采用运行简单基准测试的方法来代替。这种测试将会说明运行一段代码需要多少秒数(或者无论什么时间单位)。
基准法测试是使用时间测试的方法来衡量运行完整算法所花费的时间长度。如同科学一样,基准测试也像是一门艺术,而且为了获得精准分析需要很小心测试代码的方法。下面就来进行详细讨论。
1.1一个简单化的时间测试
首先时间测试需要一此代码。出于简单的考虑,这里将测试一个有关控制台数组内容的子程序。代码如下所示:
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环境下运行时间代码是完全不合适的。为什么呢?
首先,代码测量的是从子程序调用开始到子程序返回主程序之间流失的时间。但是测试所测量的时间也包含了与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.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
参考书箱:<<数据结构与算法>>