现代处理器由指令控制单元(ICU)和执行单元(EU)组成,ICU负责取指、译码,EU负责指令执行,为了并行处理ICU会预取指令,因此EU执行的指令一般ICU在前期就取出译码,对于含有分支的代码就包含多条取指路径,ICU在分支路径采用了分支预测技术,既预先选择一条分支取指译码,若是后期发现预测失败则须要从新取指,这意味着浪费了几个时钟周期。分支预测算法一般基于之前运行的结果来做为下次预测的依据。linux
1. 优化分支预测:算法
因为预测算法一般基于之前的结果,若是咱们的代码分支条件的判断是有规律的,则有助于分支预测的准确性,以下例所示:数组
#include <stdlib.h> #include <time.h> #include <stdio.h> #define arraySize 10000000 int array[arraySize] = {0}; int searchInArray(int value) { int sum = 0; for(int i=0; i<arraySize;i++) { if(value > array[i]) { sum+=array[i]; } } return sum; } int main() { clock_t start; clock_t end; int sum = 0; //有序数组查找 for(int i=0; i<arraySize; i++) { array[i] = i; } start = clock(); sum += searchInArray(500); end = clock(); printf("Time Expense is %d\n",end-start); printf("sum is %d\n",sum); //无序数组查找 for(int i=0; i<arraySize; i++) { array[i] = rand()%1000; } start = clock(); sum += searchInArray(500); end = clock(); printf("Time Expense is %d\n",end-start); printf("sum is %d\n",sum); return 0; }
在这个例子中咱们对数组中大于指定值的元素求和,分别对有序数组和无序数组进行测试,无序数组的时间消耗基本是有序数组的三倍左右,这里是因为if(value > array[i])分支预测中,若是数组已经有序,基于之前的预测结果后续分支预测基本都会成功,若是是无序数组则后续的预测没有依据。这里要注意对于编译期能够肯定值的无序数组,一般编译器会进行优化。在linux中咱们一般可使用likely、unlikely来优化指令的预测,likely表示很可能发生, unlikely表示不怎么发生,一样也是基于分支预测的原理。例如在入参指针判断上可使用if(unlilely(NULL == ptr))来优化。缓存
2. 避免分支预测:性能优化
在代码层面实现一样的功能,也能够经过使用不一样的代码编写方式来避免一些分支预测,一个例子就是双重for循环的问题,入下例所示:ide
int main() { clock_t start; clock_t end; int i = 0; int j = 0; start = clock(); for(i=0; i<100000000; i++) { for(j=0; j<1; j++); } end = clock(); printf("Time Expense is %d\n",end-start); start = clock(); for(i=0; i<1; i++) { for(j=0; j<100000000; j++); } end = clock(); printf("Time Expense is %d\n",end-start); return 0; }
对于上面的例子的双重循环,当外层循环次数大于内层循环时,耗时基本是后者的两倍,这是因为对于for(i=0; i<m; i++)这种分支预测,存在预测失败的可能,咱们假设一个for循环分支预测失败x次,那么若是外层循环m次内层循环n次,整个双重循环的预测失败次数是x*m+x次。能够看到外层循环次数越多其分支预测的失败次数也越多。性能
程序运行期间的数据来自存储器,现代计算的存储器采用分层设计,越上层靠近CPU的存储设备存取速度越快,上层做为下层数据的缓存是其的一个子集,从上层到下层一般多是寄存器->cache->主存->磁盘。这使得咱们在编写代码的时候能够经过考虑数据存放的位置来优化数据的存取性能。入下例所示:测试
void Summation1(int* pSum, int size) { for(int i=0; i<size-1; i++) { pSum[i+1] += pSum[i]; } return; } void Summation2(int* pSum, int size) { int temp; temp = 0; for(int i=0; i<size-1; i++) { temp += pSum[i]; } pSum[size-1] = temp; return; } int sum1[5000000] = {1,2,3,4,5,6,7,8,9,0}; int sum2[5000000] = {1,2,3,4,5,6,7,8,9,0}; int main() { clock_t start; clock_t end; int i = 0; start = clock(); Summation1(sum1,5000000); end = clock(); printf("Time Expense is %d\n",end-start); start = clock(); Summation2(sum2,5000000); end = clock(); printf("Time Expense is %d\n",end-start); return 0; }
对于一样的求和功能实现,Summation2比Summation1性能优化三倍左右,这是因为Summation1中咱们是直接对指针操做,也就是直接对cache进行读写,每次循环要进行两次读一次写cache,而在Summation2中咱们把经过使用局部变量把对cache的读写转换为寄存器的读写,每次循环只有一次cache读和一次寄存器的读写。优化
因为计算机存储的分层设计,程序在访问数据的时候先在上层的较快的存储设备中查找,没有命中再依次到下层存储设备中查找,下层命中的数据将传递给上层,不一样层之间以块大小进行传输,块的大小由层间约定。以下图所示:spa
程序在查找地址1的数据时上层存储设备没有缓存没有命中,而后在下层存储中找到该数据,下层存储将地址1所在块(假设大小为4)的数据所有传送给上层缓存。上层缓存一次缓存一整块数据。基于这个原理就能够引入了程序设计中局部性原理,咱们在设计程序的时候应该访问最近引用数据相邻的数据(空间局部性)和引用最近引用过的数据(时间局部性)。上图中咱们若是引用二、三、4都会直接在上层命中,一样若是咱们一直引用1也会一直在上层命中。一个常见的例子就是二维数组的访问问题,入下例所示:
int array1[50000][1000]; int array2[50000][1000]; int main() { clock_t start; clock_t end; int i = 0; int j = 0; int sum = 0; start = clock(); for(i=0; i< 50000; i++) { for(j=0; j<1000; j++) { sum+=array1[i][j]; } } end = clock(); printf("Time Expense is %d\n",end-start); printf("sum is %d\n",sum); start = clock(); for(i=0; i< 1000; i++) { for(j=0; j<50000; j++) { sum+=array2[j][i]; } } end = clock(); printf("Time Expense is %d\n",end-start); printf("sum is %d\n",sum); return 0; }
例子中的两种循环访问方式都是对二维数组进行循环求和,但前者的效率要比后者高出二十倍左右,前者对数组的访问方式以下左图,后者对数组的访问方式以下右图,根据程序的局部性访问原理咱们能够很容易的看到后者的访问方式会致使上层缓存常常不命中。
这里能够看到前面的循环虽然分支预测失败的次数可能更多,但效率仍是要好不少,说明存储器的访问速度是制约程序性能的主要瓶颈。
以上代码的都是在笔者的机器上测试,不一样的环境可能存在差别。
转载请注明原始出处:http://www.cnblogs.com/chencheng/p/34002