您的位置:首页 > 编程语言 > C#

0.033秒的艺术 --- 测试程序性能

2008-11-28 00:51 190 查看
这是一系列关于C#与游戏编程性能提示的文章,先来看看如何简单测试一段C#程序的性能。

 0.033秒的艺术 --- 测试程序性能
仅供个人学习使用,请勿转载,勿用于任何商业用途。
  
    交互式的实时3D程序每秒至少渲染30帧图像,才能保证视觉上的平滑,这意味着所有渲染,AI,物理仿真都必须在0.033秒内完成。只有高度优化的代码和算法才能达到这样的要求。如何判断某个算法是否能满足要求,如何比较哪一种算法才是最优的呢?方法有很多,通过丢硬币,撒骰子或者凭感觉猜测等等,不过这些方法通常需要一点点运气的帮助。可惜我不是每天都能有那么好的运气,所以测试是我通常选择的方法。永远不要在测试结束之前作出任何结论。

         几乎所有的测试都以时间和空间代价为主要衡量标准。然而考虑到目前2G内存已经降到了130米以下的价格,所以这里我只关心时间。但是也不要忘了程序和数据所占的空间越小,越能获得缓存所带来的好处,提高命中率。如何来测量一段程序的执行时间呢?啊,你可能马上想到了System.Times和System.Windows.Forms名称空间下的Timer或许还有DateTime。可惜这些计时器的精度都不太理想,通常几十毫秒才更新一次。我们需要高精度的时钟,这将用到2个win32 API:

QueryPerformanceCounter()   这个函数返回硬件支持的高精度计时器的值。
QueryPerformanceFrequency()   返回硬件计时器的频率。

         通过直接访问硬件,这是一个精度非常高的时钟,可以把它的返回值想象为CPU的晶震数和晶振频率。在测试开始和结束的时候,分别查询一次计时器的值,最后相减除以计时器的频率,就能得到精确的时间间隔,以秒为单位,当然,可以进一步转换为任何你觉得方便的单位。可以说目前绝大部分3D游戏的计时系统都是以这2个函数为基础。为了在C#下使用他们,需要通过P/Invoke导入这这两个函数。好了,现在可以这样来测试一段程序的性能:

class Test

{

    [System.Runtime.InteropServices.DllImport("Kernel32.dll")]

    private static extern bool QueryPerformanceCounter(out long lpPerformanceCount);

    [System.Runtime.InteropServices.DllImport("Kernel32.dll")]

    private static extern bool QueryPerformanceFrequency(out long lpFrequency);

    static void Main(string[] args)

    {

       long startTime, endTime, freq;

       QueryPerformanceFrequency(out freq);

       QueryPerformanceCounter(out startTime);

    //Do your test here

       QueryPerformanceCounter(out endTime);            

       Console.WriteLine("frequency: {0:n0}", freq);

       Console.WriteLine("time: {0:n5}s", (endTime - startTime)/(double)freq);

       Console.Read();

    }

}

     也许是这样的代码太过普通和频繁,.net 2.0中,加入了对这个高精度计时器的支持。不再需要P/Invoke,只需记住System.Diagnostics.Stopwatch。现在可以把上面的代码简化为:

class TestClass

{

    static void Main(String[] args)

    {

        Stopwatch timer = new Stopwatch();

        timer.Start();

        //Do your test here

        timer.Stop();

        Console.WriteLine(timer.ElapsedMilliseconds.ToString());

    }

}

         简单多了不是吗。但这段程序还不够好,测试代码之前的代码可能产生了很多垃圾,而当你开始测试时,垃圾回收发生了,显然,这是不是我们所希望的。虽然我们不能控制GC精确的执行时间,但可以把GC对测试代码的影响减小到最低程度:

class TestClass

{

    static void Main(String[] args)

    {

       Stopwatch timer = new Stopwatch();

       // initialize your code here

       

       GC.Collect(2);

       GC.WaitForPendingFinalizers();

       GC.Collect(2);

       GC.WaitForPendingFinalizers();

        timer.Start();

        //Do your test here

        timer.Stop();

        Console.WriteLine(timer.ElapsedMilliseconds.ToString());

    }

}

        在测试之前强制垃圾回收。WaitForPendingFinalizers()将会挂起当前线程,直到GC完全结束。注意,这里强制进行了2次垃圾回收。如果你对垃圾回收有所了解的话,那么应该知道当GC发现一个对象有Finalize()方法时,会先调用Finalize(),然后到下一次GC发生时才回收这个对象,我们确保在测试之前所有垃圾都已经回收释放了。

         好了,现在我们有了一个简单实用的测试模板,不过还有几点忠告是在测试时必须记住的 :
1. 在测试结束之前永远不要下结论
2. 在测试之前,至少执行一次测试代码。比如你的测试代码是Test(),那么在timer.start()前至少执行一次Test(),排除JIT对测试结果的影响。
3. 多运行几次程序,取平均值作为测试结果
4. 不要在Debug模式下执行测试
5. 不要在测试代码中输出测试信息,Console是相对较慢的操作。
6. 不同的CLR和编译器版本下的测试结果可能会不同。
以上忠告都来自好心的Tobias Hertkorm
         
         现在你已经成为性能分析大师了。不要再到论坛上问究竟是for还是foreach快这样的问题了,自己动手试试吧。当然,如果你比我还懒,那么下次来看我对for和foreach的分析结果吧 :)

  
  
 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息