优化程序要点

以下内容总结自《深入理解计算机系统》一书。

优化程序性能

编写高效程序的要点

  1. 选择一组适当的算法和数据结构
  2. 写出编译器能够有效优化以转换成高效可执行代码的源代码

程序优化的步骤

消除不必要的工作

消除不必要的函数调用、条件测试和内存引用

提高并行性

处理器具有指令级并行能力,同时执行多条指令。通过循环展开、增加累计变量、改变操作顺序等方式增加代码的并行度。

使用代码剖析程序

优化实例

以下代码中,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;
}

总结

  1. 尽量不要重复调用返回相同结果的函数,特别是那些时间复杂度不为O(1)的函数。
  2. 多使用局部变量保存计算的中间结果。
  3. 将不变的变量之间的操作放在一起,尽量减少操作之间的依赖性。
posted @ 2020-03-23 11:18  luoheng  阅读(269)  评论(0编辑  收藏  举报