5.5 减少过程调用

5.5 减少过程调用

像我们看到过的那样,过程调用会带来开销,而且妨碍大多数形式的程序优化。从 combine2 的代码(见图 5-6)中我们可以看出,每次循环迭代都会调用 get_vec_element 来获取下一个向量元素。对每个向量引用,这个函数要把向量索引 i 与循环边界做比较,很明显会造成低效率。在处理任意的数组访问时,边界检查可能是个很有用的特性,但是对 combine2 代码的简单分析表明所有的引用都是合法的。

作为替代,假设为我们的抽象数据类型增加一个函数 get_vec_start。这个函数返回数组的起始地址,如图 5-9 所示。然后就能写出此图中 combine3 所示的过程,其内循环里没有函数调用。它没有用函数调用来获取每个向量元素,而是直接访问数组。一个纯粹主义者可能会说这种变换严重损害了程序的模块性。原则上来说,向量抽象数据类型的使用者甚至不应该需要知道向量的内容是作为数组来存储的,而不是作为诸如链表之类的某种其他数据结构来存储的。比较实际的程序员会争论说这种变换是获得高性能结果的必要步骤。

函数 方法 整数 + 整数 * 浮点数 + 浮点数 *
combine2 移动 vec_length 7.02 9.03 9.02 11.03
combine3 直接数据访问 7.17 9.02 9.02 11.03
data_t *get_vec_start(vec_ptr v)
{
    return v->data;
}

/* Direct access to vector data */
void combine3(vec_ptr v, data_t *dest)
{
    long i;
    long length = vec_length(v);
    data_t *data = get_vec_start(v);

    *dest = IDENT;
    for (i = 0; i < length; i++) {
        *dest = *dest OP data[i];
    }
}

图 5-9 消除循环中的函数调用。结果代码没有显示性能提升,但是它有其他的优化。

令人吃惊的是,性能没有明显的提升。事实上,整数求和的性能还略有下降。显然,内循环中的其他操作形成了瓶颈,限制性能超过调用 get_vec_element。我们还会再回到这个函数(见 5.11.2 节),看看为什么 combine2 中反复的边界检查不会让性能更差。而现在,我们可以将这个转换视为一系列步骤中的一步,这些步骤将最终产生显著的性能提升。