5.12.1 加载的性能
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 节中讨论。