Tutorial performance and time

原文链接:Tutorial performance and time

在讨论性能之前,先讨论一个重要的话题:时间。为了理解代码中的变化如何影响性能,我们需要一个排序的指标。有许多方法用于时间例程,一些比另一些合适。在本教程中我们将讨论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

这非常简单

1
2
3
#include <stdint.h>
uint64_t start = mach_absolute_time();
uint64_t stop = mach_absolute_time();

这样就可以了。我们通常获取两个值,以得到这两个时间的时间差。

将mach_absolute_time时间差转换为秒数

这稍微有点复杂,因为我们需要获取mach_absolute_time所基于的系统时间基准。如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#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来编译它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#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)机器上,我获得如下结果:

1
2
3
4
5
6
7
8
[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)。内存清零是一种安全预防措施(我们不需要递交一些包含安全信息的内容,如解密密钥)

清零过程产生一个副作用(被系统标记为零填充页面故障)。所以为了让我们的计时更精确些,我们在使用内存之前一次性填充数据,以确保我们不会获取到零填充页面故障的处理时间。

让我们来测试一下,注释下面这行代码

1
for(i=0;i<size;i++) array =(double)i;

为:

1
//for(i=0;i<size;i++) array =(double)i;

再次运行测试

1
2
3
4
5
6
7
[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。再次说明内存被清零了。如果我们从堆上获取了内存,我们获取到的是无意义的数值。

最后,但很重要的一点。不要依赖于内存的清零操作。很有可能获取到的内存是从一个静态分配区而来,那么可能会导致如下这样的问题

1
double array[3][3];

在我的系统上的打印结果是:

1
2
3
-1.99844 -1.29321e-231 -1.99844
-3.30953e-232 -5.31401e+303 0
1.79209e-313 3.3146e-314 0

所以需要特别注意