来自会员德鲁伊: 在讨论性能之前,先讨论一个重要的话题:时间。为了理解代码中的变化如何影响性能,我们需要一个排序的指标。有许多方法用于时间例程,一些比另一些合适。在本教程
“ ”来自会员德鲁伊:
在讨论性能之前,先讨论一个重要的话题:时间。为了理解代码中的变化如何影响性能,我们需要一个排序的指标。有许多方法用于时间例程,一些比另一些合适。在本教程中我们将讨论Mach Absolute Time。
为什么是Mach?
时间例程依赖于所需要测量的时间域。某些情况下使用诸如clock()或getrusage()函数来做些简单的数学运算就足够了。如果时间例程将用于实际的开发框架之外,可移植性就很重要了。我不使用这些。为什么?
对于我来说,调试代码的典型问题是:
1) 我只需要在即时测试时使用时间例程
2) 我不喜欢依赖于多种函数来包含不同的时间域。它们的行为可能不一致
3) 有时我需要一个高精度定时器
欢迎了解mach_absolute_time
mach_absolute_time是一个CPU/总线依赖函数,返回一个基于系统启动后的时钟”嘀嗒”数。它没有很好的文档定义,但这不应该成为使用它的障碍,因为在MAC OS X上可以确保它的行为,并且,它包含系统时钟包含的所有时间区域。那是否应该在产品代码中使用它呢?可能不应该。但是对于测试,它却恰到好处。
使用mach_absolute_time时需要考虑两个因素:
1) 如何获取当前的Mach绝对时间
2) 如何将其转换为有意义的数字
获取mach_absolute_time
这非常简单
- #include <stdint.h>
- uint64_t start =mach_absolute_time();
- uint64_t stop =mach_absolute_time();
这样就可以了。我们通常获取两个值,以得到这两个时间的时间差。
将mach_absolute_time时间差转换为秒数
这稍微有点复杂,因为我们需要获取mach_absolute_time所基于的系统时间基准。如下代码:
- #include <stdint.h>
- #include<mach/mach_time.h>
- //Raw mach_absolute_times going in,difference in seconds out
- double subtractTimes( uint64_tendTime, uint64_t startTime )
- {
- uint64_t difference = endTime - startTime;
- static double conversion = 0.0;
- if( conversion == 0.0 )
- {
- mach_timebase_info_data_t info;
- kern_return_t err =mach_timebase_info( &info );
- //Convert the timebase into seconds
- if( err == 0 )
- conversion= 1e-9 * (double) info.numer / (double) info.denom;
- }
- return conversion * (double)difference;
- }
这里最重要的是调用mach_timebase_info。我们传递一个结构体以返回时间基准值。最后,一旦我们获取到系统的心跳,我们便能生成一个转换因子。通常,转换是通过分子(info.numer)除以分母(info.denom)。这里我乘了一个1e-9来获取秒数。最后,我们获取两个时间的差值,并乘以转换因子,便得到真实的时间差。
现在我们可能会想,为什么这比用clock好?看起来做了更多的事情。确实是有点,这便是为什么它在一个函数中。我们只需要传递我们的值到函数中并取得答案。
例子
让我们写个例子。下面是完整的代码清单(包括mach函数)。可以使用gcc mach.c –o mach来编译它:
- #include <stdio.h>
- #include <stdlib.h>
- #include <stdint.h>
- #include <math.h>
- #include<mach/mach_time.h>
- //Raw mach_absolute_times going in,difference in seconds out
- double subtractTimes( uint64_tendTime, uint64_t startTime )
- {
- uint64_t difference = endTime -startTime;
- static double conversion = 0.0;
- if( conversion == 0.0 )
- {
- mach_timebase_info_data_tinfo;
- kern_return_terr = mach_timebase_info( &info ); //Convert the timebaseinto seconds
- if(err == 0 )
- conversion= 1e-9 * (double) info.numer / (double) info.denom;
- }
- return conversion * (double)difference;
- }
- int main()
- {
- inti, j, count;
- uint64_t start,stop;
- doublecurrent = 0.0;
- doubleanswer = 0.0;
- doubleelapsed = 0.0;
- intdim1 = 256;
- intdim2 = 256;
- intsize = 4*dim1*dim2;
- //Allocatesome memory and warm it up
- double *array =(double*)malloc(size*sizeof(double));
- for(i=0;i<size;i++)array = (double)i;
- count= 5;
- for(i=0;i<count;i++)
- {
- start = mach_absolute_time();
- //dosome work
- for(j=0;j<size;j++){
- answer+= sqrt(array[j]);
- }
- stop =mach_absolute_time();
- current= subtractTimes(stop,start);
- printf("Timefor iteration: %1.12lf for answer: %1.12lf\n",current, answer);
- elapsed+= current;
- }
- printf("\nTotaltime in seconds = %1.12lf for answer: %1.12lf\n",elapsed/count,answer);
- free(array);
- return 0;
- }
我们在这里做了什么?在这个例子中,我们有一个适当大小的double数组,当中存放了一些数字,然后获取这些数值的和的开方。为了测试,我们迭代了5次这个计算。每次迭代后我们打印花费的时间,并总结了计算所需的运行时间。在我的PowerMac G5(2.5)机器上,我获得如下结果:
- [bigmac:~/misc] macresearch% gcc mach.c -omach
- [bigmac:~/misc] macresearch%./mach
- Time for iteration: 0.006717496412for answer: 89478229.125529855490
- Time for iteration: 0.007274204955for answer: 178956458.251062750816
- Time for iteration: 0.006669191332for answer: 268434687.376589745283
- Time for iteration: 0.006953711252for answer: 357912916.502135872841
- Time for iteration: 0.007582157340for answer: 447391145.627681851387
- Average time in seconds =0.007039352258 for answer: 447391145.627681851387
注意,在这里我没有进行优化,因为编译器有方法避开这样的无脑循环。另外,这只是一个例子。如果是真正的代码,我们会进行优化。
好了,这就是这个例子的两个目的。
首先,我使用的数组大小比我的缓存大。我这样做的目的是因为我们需要注意到数据溢出缓存的情况(正如这个例子一样,至少在我的系统中是这样。如果是在MacPro中,不会出现这种情况)。我们将在以后讨论缓存的事宜。当然,这是一个做作的例子,但有一些东西可供思考。其次,你注意到在内存分配之前我写了一句注释,这是什么意思呢?
这在实际情况下是不需要关心的事情,因为内存总是在需要时已准备好使用。但当做一些小测试时来测试函数的性能时,它却可能是会影响到测试结果的实际问题。
当动态分配内存时,第一次访问内存管理时会将其清0(在OS X中不管使用哪种动态分配函数:malloc, calloc…所有内存在用户使用前都会清0)。内存清零是一种安全预防措施(我们不需要递交一些包含安全信息的内容,如解密密钥)
清零过程产生一个副作用(被系统标记为零填充页面故障)。所以为了让我们的计时更精确些,我们在使用内存之前一次性填充数据,以确保我们不会获取到零填充页面故障的处理时间。
让我们来测试一下,注释下面这行代码
for(i=0;i<size;i++) array =(double)i;
为:
//for(i=0;i<size;i++) array =(double)i;
再次运行测试
- [bigmac:~/misc] macresearch% ./mach
- Time for iteration: 0.009478866798for answer: 0.000000000000
- Time for iteration: 0.004756880234for answer: 0.000000000000
- Time for iteration: 0.004927868215for answer: 0.000000000000
- Time for iteration: 0.005227029674for answer: 0.000000000000
- Time for iteration: 0.004891864428for answer: 0.000000000000
- Average time in seconds =0.005856501870 for answer: 0.000000000000
注意第一次迭代的时间比后序的时间多了将近一倍。同时还需要注意所有的answer都是0。再次说明内存被清零了。如果我们从堆上获取了内存,我们获取到的是无意义的数值。
最后,但很重要的一点。不要依赖于内存的清零操作。很有可能获取到的内存是从一个静态分配区而来,那么可能会导致如下这样的问题。
double array[3][3];
在我的系统上的打印结果是:
-1.99844 -1.29321e-231 -1.99844
-3.30953e-232 -5.31401e+303 0
1.79209e-313 3.3146e-314 0
所以需要特别注意。