基础优化是硬件平台无关的优化。书上讲了几种基础的优化。
。代码移动
例如:
int i ;
for( i = 0; i < get_length( v ); i++ )//v是整型数据数组,get_length()得到数组长度
{
printf( get_element( i ) );//get_element()得到i位置的数组元素
}
每次循环都要调用get_length()函数,cpu耗费一些时间在函数的call,ret指令和参数入栈所执行的操作上,而这个函数返回的结果不会随着循环的操作而改变。
只要添加一个临时整型变量记录get_length()返回的值就可以节省这个开销:
int i ;
int length = get_length(v);
for( i = 0; i < length; i++ )//v是某种数据类型的数组
{
printf( get_element( i ) );
}
。减少函数调用
可以看到每次循环都调用get_element( )来获取数组的元素。这个做法的好处是数据结构的抽象,调用者可以不必知道v的实际存储方法,它可以用数组或者是链表来存储,或者是其它的数据结构 。代价是付出函数调用的开销。
但是如果这种数据抽象与执行性能比起来并不重要的话可以用数组下标直接访问这个数组的元素:
int i ;
for( i = 0; i < get_length( v ); i++ )//v是整型数据数组,get_length()得到数组长度
{
printf( v[i] );
}
这样会破坏掉数据结构的封装性,将来v的存储方式发生变化,这段代码也需要相应修改。
。消除不必要的存储器引用
例如:
fun( vec_ptr v, data_t *dest )
{
....
data_t * data = get_start( v );
*dest = 1;
for( i = 0; i < length; i++ )
{
*dest *= data[i];
}
}
这段代码计算数组中元素的乘积。例如v是两个元素的数组{2 ,3},那么我们期望最后得到的结果是1*2*3等于6。
但是这段代码对编译器来说有个歧义,它并不如我们想当然的那样看待这段代码。
这是因为,如果参数dest是指向v[1](数值是3)的指针,函数执行完结果就变成了4,执行的过程中改变了v[1]的值。
保守的编译器不会排除上面这种情况的可能性,所以,每次相乘之后,它会先把值写到dest指向的存储器位置,然后在下一次循环的时候再把值从存储器读出来。汇编代码如下:
.L18:
movl (%edi), %eax ;从dest所指位置读出值
imull ( %ecx, %edx, 4 ) ,%eax ;*dest * v[i]
movl %eax, ( %edi ) ;把结果写回dest所指位置
incl %edx ;增加i
cmpl %esi , edx ;i与length对比
j1 .L18 ;根据上一条指令设置的条件码判断是否进行下一次循环
PS:按照我个人的看法,编译器只需要把 movl (%edi), %eax 指令放在循环外就可以达到它保守的目的,因为%eax写入存储器后循环代码中没有其它操作来覆盖%eax的值,所以下一次循环直接用%eax的值就可以了,何必多此一举,这是一个让我不解的地方。
这段代码用了 movl (%edi), %eax 和 movl %eax, ( %edi )来保证*dest的即使更新。但是如果dest所指的位置与v无关,根本就不需这么反复地更新*dest。
所以可以在c代码中添加一个临时变量x,来保存每次循环的结果,告诉编译器执行循环的时候不会改变v中元素的值:
fun( vec_ptr v, data_t *dest )
{
....
data_t * data = get_start( v );
data_t x = 1;
for( i = 0; i < length; i++ )
{
x *= data[i];
}
*dest = x;
}
这样一来编译器生成下面的代码:
.L24:
imull ( %eax, %edx, 4), %ecx;x * v[i]
incl %edx ;增加i
cmpl %esi, %edx;i与length对比
j1 .L24;根据上一条指令设置的条件码判断是否进行下一次循环
省去了1条存储器读和1条存储器写指令。