这是一系列关于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导入这这两个函数。好了,现在可以这样来测试一段程序的性能:

 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。现在可以把上面的代码简化为:

 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对测试代码的影响减小到最低程度:

 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的分析结果吧 :)

相关文章:

  • 2021-07-04
  • 2021-10-24
  • 2022-12-23
  • 2022-12-23
  • 2022-01-04
  • 2021-06-19
  • 2021-12-09
  • 2021-06-25
猜你喜欢
  • 2022-01-13
  • 2022-01-30
  • 2021-12-22
  • 2021-06-18
  • 2021-06-10
  • 2021-12-23
  • 2021-07-20
相关资源
相似解决方案