# 5.12.1 加载的性能

一个包含加载操作的程序的性能既依赖于流水线的能力,也依赖于加载单元的延迟。在参考机上运行合并操作的实验中,我们看到除了使用 SIMD 操作时以外,对任何数据类型组合和合并操作来说,CPE 从没有到过 0.50 以下。一个制约示例的 CPE 的因素是,对于每个被计算的元素,所有的示例都需要从内存读一个值。对两个加载单元而言,其每个时钟周期只能启动一条加载操作,所以 CPE 不可能小于 0.50。对于每个被计算的元素必须加载 k 个值的应用,我们不可能获得低于 k/2 的 CPE(例如参见家庭作业 5.15)。

到目前为止,我们在示例中还没有看到加载操作的延迟产生的影响。加载操作的地址只依赖于循环索引 i,所以加载操作不会成为限制性能的关键路径的一部分。

要确定一台机器上加载操作的延迟,我们可以建立由一系列加载操作组成的一个计算,一条加载操作的结果决定下一条操作的地址。作为一个例子,考虑图 5-31 中的函数 list_len,它计算一个链表的长度。在这个函数的循环中,变量 ls 的每个后续值依赖于指针引用 ls->next 读出的值。测试表明函数 list_len 的 CPE 为 4.00,我们认为这直接表明了加载操作的延迟。

typedef struct ELE {
    struct ELE *next;
    long data;
} list_ele, *list_ptr;

long list_len(list_ptr ls)
{
    long len = 0;
    while (ls) {
        len++;
        ls = ls->next;
    }
    return len;
}

图 5-31 链表函数。其性能受限于加载操作的延迟。

要弄懂这一点,考虑循环的汇编代码:

# Inner loop of list_len
# ls in %rdi, len in %rax
.L3:                       # loop:
    addq  $1, %rax         # Increment len
    movq  (%rdi), %rdi     # ls = ls->next
    testq %rdi, %rdi       # Test ls
    jne   .L3              # If nonnull, goto loop

第 3 行上的 movq 指令是这个循环中关键的瓶颈。后面寄存器 %rdi 中的每个值都依赖于加载操作的结果,而加载操作又以 %rdi 中的值作为它的地址。因此,直到前一次迭代的加载操作完成,下一次迭代的加载操作才能开始。这个函数的 CPE 等于 4.00,是由加载操作的延迟决定的。事实上,这个测试结果与文档中参考机的 L1 级 cache 的 4 周期访问时间是一致的,相关内容将在 6.4 节中讨论。