原文链接:Tutorial performance and time
在讨论性能之前,先讨论一个重要的话题:时间。为了理解代码中的变化如何影响性能,我们需要一个排序的指标。有许多方法用于时间例程,一些比另一些合适。在本教程中我们将讨论Mach Absolute Time
。
为什么是Mach?
时间例程依赖于所需要测量的时间域。某些情况下使用诸如clock()
或getrusage()
函数来做些简单的数学运算就足够了。如果时间例程将用于实际的开发框架之外,可移植性就很重要了。我不使用这些。为什么?
对于我来说,调试代码的典型问题是:
- 我只需要在即时测试时使用时间例程
- 我不喜欢依赖于多种函数来包含不同的时间域。它们的行为可能不一致
- 有时我需要一个高精度定时器
欢迎了解mach_absolute_time
mach_absolute_time
是一个CPU
/总线依赖函数,返回一个基于系统启动后的时钟”嘀嗒”数。它没有很好的文档定义,但这不应该成为使用它的障碍,因为在MAC OS X
上可以确保它的行为,并且,它包含系统时钟包含的所有时间区域。那是否应该在产品代码中使用它呢?可能不应该。但是对于测试,它却恰到好处。
使用mach_absolute_time
时需要考虑两个因素:
- 如何获取当前的
Mach
绝对时间 - 如何将其转换为有意义的数字
获取mach_absolute_time
这非常简单
|
|
这样就可以了。我们通常获取两个值,以得到这两个时间的时间差。
将mach_absolute_time时间差转换为秒数
这稍微有点复杂,因为我们需要获取mach_absolute_time
所基于的系统时间基准。如下代码:
|
|
这里最重要的是调用mach_timebase_info
。我们传递一个结构体以返回时间基准值。最后,一旦我们获取到系统的心跳,我们便能生成一个转换因子。通常,转换是通过分子(info.numer
)除以分母(info.denom
)。这里我乘了一个1e-9
来获取秒数。最后,我们获取两个时间的差值,并乘以转换因子,便得到真实的时间差。
现在我们可能会想,为什么这比用clock
好?看起来做了更多的事情。确实是有点,这便是为什么它在一个函数中。我们只需要传递我们的值到函数中并取得答案。
例子
让我们写个例子。下面是完整的代码清单(包括mach
函数)。可以使用gcc mach.c –o mach
来编译它:
|
|
我们在这里做了什么?在这个例子中,我们有一个适当大小的double
数组,当中存放了一些数字,然后获取这些数值的和的开方。为了测试,我们迭代了5
次这个计算。每次迭代后我们打印花费的时间,并总结了计算所需的运行时间。在我的PowerMac G5(2.5)
机器上,我获得如下结果:
|
|
注意,在这里我没有进行优化,因为编译器有方法避开这样的无脑循环。另外,这只是一个例子。如果是真正的代码,我们会进行优化。
好了,这就是这个例子的两个目的。
首先,我使用的数组大小比我的缓存大。我这样做的目的是因为我们需要注意到数据溢出缓存的情况(正如这个例子一样,至少在我的系统中是这样。如果是在MacPro
中,不会出现这种情况)。我们将在以后讨论缓存的事宜。当然,这是一个做作的例子,但有一些东西可供思考。其次,你注意到在内存分配之前我写了一句注释,这是什么意思呢?
这在实际情况下是不需要关心的事情,因为内存总是在需要时已准备好使用。但当做一些小测试时来测试函数的性能时,它却可能是会影响到测试结果的实际问题。
当动态分配内存时,第一次访问内存管理时会将其清0
(在OS X
中不管使用哪种动态分配函数:malloc
, calloc
…所有内存在用户使用前都会清0
)。内存清零是一种安全预防措施(我们不需要递交一些包含安全信息的内容,如解密密钥)
清零过程产生一个副作用(被系统标记为零填充页面故障)。所以为了让我们的计时更精确些,我们在使用内存之前一次性填充数据,以确保我们不会获取到零填充页面故障的处理时间。
让我们来测试一下,注释下面这行代码
|
|
为:
|
|
再次运行测试
|
|
注意第一次迭代的时间比后序的时间多了将近一倍。同时还需要注意所有的answer
都是0
。再次说明内存被清零了。如果我们从堆上获取了内存,我们获取到的是无意义的数值。
最后,但很重要的一点。不要依赖于内存的清零操作。很有可能获取到的内存是从一个静态分配区而来,那么可能会导致如下这样的问题
|
|
在我的系统上的打印结果是:
|
|
所以需要特别注意