如何编写高效的代码(2014/6/1)

编写高效的代码有两个条件:选择好的算法和数据结构,编写编译器能够优化以转换成高效可执行的代码。前者是基础和前提,即使后者做的足够好,但是选用了错误的算法和数据结构,优化也不起作用,这个一点要搞清楚。本文的内容的侧重于后者。

1 计算机系统架构


L1和L2位于CPU芯片上,L3被各个CPU共享。由于成本的考虑,L3,L2,L1的大小依次递减。以INTEL XEON E7-8891V2为例,L3 37.5 MBL2 2.5 MBL1 640 KB

2 各种存储的速度比

寄存器

L1

10-13

L2

10-12

L3

10-11

内存

10-10

磁盘

10-3

 从上表中可以看出,内存比磁盘快1000万倍。

3 进程的内存分布

共享映射库的起始地址是0x40000000(1G),BSS: 未初始化的全局变量。

4 函数调用的开销

 

ebp寄存器指向当前帧的栈底,esp寄存器指向当前帧的栈顶。函数被调用时,都会生成一个新的帧。返回地址:函数调用返回调用者后,待执行的代码的地址。在调用函数之前,调用者在自己的栈内构造参数。缓冲区溢出攻击的原理:覆盖被保存的ebp和返回地址,执行攻击者指定的函数。

5 代码优化

编写高效的代码有两个步骤:选择好的算法和数据结构,编写编译器能够优化以转换成高效可执行的代码。第一个步骤是前提。即使第二部做的足够好,但是选用了错误的算法和数据结构,优化也不起作用。本小节后续的优化措施指的是后者。

5.1 函数调用和系统调用

(1) 从第四小节可以看出,函数调用的开销不菲。所以在调用次数非常高的函数内,尽量少的使用函数,特别是小函数。

(2) 尽量把循环内部的函数调用移到循环外部。

带来的副作用损害了代码的可读性,可维护性。

系统调用的的开销:

1,段的切换。

2,当前数据、指令预取队列的刷新和重建。

3,数据复制。

5.2 利用内存局部性原理

时间局部性:如果数据被访问,则不久之后该数据可能被再次访问。

空间局部性:如果某个存储单元被访问,则不久之后附近的存储单元也会被访问。

例:没有利用空间局部性的代码

for(i=0;i<M;i++)
for(j=0;j<N;j++)
    sum += a[j][i];

例:利用空间局部性的代码

for(i=0;i<M;i++)
for(j=0;j<N;j++)
    sum += a[i][j];

重复利用同一变量有良好的时间局部性。

5.3 适应CPU的指令流水线

执行指令的基本流程:

1 从内存读取指令到L1

2 对L1中的指令译码

3 执行单元执行指令

现代的CPU进行分支预测,在指令高速缓存区形成一个指令流水线,如果预测失败,需要重新从内存读取指令,译码形成一个新的指令流水线,对CPU而言,操作内存的开销非常大(2个数量级的差别),所以代价非常高。

优化措施:

(1) 尽量减少分支

尽量不用if/else  switch。用switch代替if/else,因为跳转指令更少。

经过测试if 和?:的汇编代码是一样的,所以性能也一样。

(2 )减少小循环,因为循环里面有跳转指令

(3)减少函数调用

猜你喜欢

转载自blog.csdn.net/liaoxiangui/article/details/80594550