优化程序要点
以下内容总结自《深入理解计算机系统》一书。
优化程序性能
编写高效程序的要点
- 选择一组适当的算法和数据结构
- 写出编译器能够有效优化以转换成高效可执行代码的源代码
程序优化的步骤
消除不必要的工作
消除不必要的函数调用、条件测试和内存引用
提高并行性
处理器具有指令级并行能力,同时执行多条指令。通过循环展开、增加累计变量、改变操作顺序等方式增加代码的并行度。
使用代码剖析程序
优化实例
以下代码中,v
为数组类型,代码的功能是将v
的元素通过OP操作累积到dest
变量中。
vec_length
函数的时间复杂度为O(n)
。详细代码可参见《深入理解计算机系统》一书。
// O(n^2)
void combine1(vec_ptr v, data_t *dest)
{
long i;
*dest = IDENT;
// 由于vec_length时间复杂度为O(n),每次判断
// 的时间复杂度为O(n),则整个循环时间复杂度为
// O(n^2)
for (i = 0; i < vec_length(v); i++)
{
data_t val;
get_vec_element(v, i, &val);
*dest = *dest OP val;
}
}
措施1:消除循环的低效率
将每次调用返回相同结果的函数移至循环外,避免重复调用导致的低效率。
// O(n)
void combine2(vec_ptr v, data_t *dest)
{
long i;
// 把vec_length移到循环外
// 字符串的strlen函数同理
long length = vec_length(v);
*dest = IDENT;
for (i = 0; i < length; i++)
{
data_t val;
get_vec_element(v, i, &val);
*dest = *dest OP val;
}
}
措施2:减少过程调用
减少循环内的函数,可以直接使用索引访问数据,减少封装函数。
void combine3(vec_ptr v, data_t *dest)
{
long i;
long length = vec_length(v);
data_t *data = v->data;
*dest = IDENT;
for (i = 0; i < length; i++)
{
*dest = *dest OP data[i];
}
}
措施3:消除不必要的内存引用
内存访问比寄存器访问慢很多,可以先使用局部变量保存结果,循环结束后写回dest
中。
void combine4(vec_ptr v, data_t *dest)
{
long i;
long length = vec_length(v);
data_t *data = v->data;
// 使用局部变量保存结果
data_t acc = IDENT;
*dest = IDENT;
for (i = 0; i < length; i++)
{
acc = acc OP data[i];
}
*dest = acc;
}
措施4:循环展开
通过增加每次迭代计算的元素数量,减少循环的迭代次数。
void combine5(vec_ptr v, data_t *dest)
{
long i;
long length = vec_length(v);
long limit = length - 1;
data_t *data = v->data;
data_t acc = IDENT;
for (i = 0; i < length; i+=2)
{
// 每次循环计算两个元素
// 这里的措施并不会提高指令的并行度
// 因为每一个OP操作都依赖于上一次OP操作的结果,
// 所以本质上指令还是串行的
acc = (acc OP data[i]) OP data[i+1];
}
// 还可能剩一个
for (; i < length; i++) {
acc = (acc OP data[i]);
}
*dest = acc;
}
措施5:多个累计变量
使用多个累计变量,配合循环展开,可以有效增加并行度。
void combine6(vec_ptr v, data_t *dest)
{
long i;
long length = vec_length(v);
long limit = length - 1;
data_t *data = v->data;
data_t acc0 = IDENT;
data_t acc1 = IDENT;
for (i = 0; i < length; i+=2)
{
// 两个累积变量没有相互依赖
// 计算机可以并行执行两个OP操作
// 指令执行时间可以缩短一半
acc0 = acc0 OP data[i];
acc1 = acc1 OP data[i+1];
}
for (; i < length; i++) {
acc0 = acc0 OP data[i];
}
*dest = acc0 OP acc1;
}
措施6:重新结合变换
合理分配操作的顺序,可以有效提高指令的并行度。
void combine7(vec_ptr v, data_t *dest)
{
long i;
long length = vec_length(v);
long limit = length - 1;
data_t *data = v->data;
data_t acc = IDENT;
for (i = 0; i < length; i+=2)
{
// 这里将括号放在了后两个数据上,
// 由于data变量在循环中从未改变,
// 所以第二个OP操作不依赖于其他的OP操作
// 因此第二个OP操作可以和其他的OP操作并行
// 结果指令执行时间缩短一半
acc = acc OP (data[i] OP data[i+1]);
}
for (; i < length; i++) {
acc = (acc OP data[i]);
}
*dest = acc;
}
总结
- 尽量不要重复调用返回相同结果的函数,特别是那些时间复杂度不为O(1)的函数。
- 多使用局部变量保存计算的中间结果。
- 将不变的变量之间的操作放在一起,尽量减少操作之间的依赖性。