CSAPP-优化程序性能
理解现代处理器
乱序执行
现代处理器能够在每个时钟周期内进行多个操作,而且是乱序执行的。这得益于两个模块:指令控制单元(ICU)、执行单元(EU)。
ICU负责提前从内存中读取指令,EU负责执行指令。具体来说,ICU将会从指令高速缓存中提前一段时间将指令取出,以便进行译码操作,然后将指令发送给EU。
译码:译码单元将复杂的指令分解为多个微操作。例如,对于指令
addq %rax, 8(%rdx)
,它可能会被分解为以下几个微操作:
- 计算地址:将
%rdx
加上偏移量8
以生成目标地址。 - 加载数据:从目标地址加载值到临时寄存器。
- 执行加法:将
%rax
的值与临时寄存器的值相加。 - 写回内存:将计算结果存回目标地址。
这些微操作通过依赖分析后,可以乱序分派给执行单元,并行完成。
分支预测
面对分支时CPU采取分支预测技术猜测是否选择分支,它有两步:
- 分支方向预测:预测是否跳转。
- 目标地址预测:预测跳转后的指令地址。
接着,CPU会在实际的分支判断完成前,直接执行预测的分支指令。如果预测错误,就返回分支点状态并执行另一条分支,这会消耗一些时间。不过现代CPU使用了多种技术来提高分支预测的准确率,因此编码时不必过分担心分支预测失败时的惩罚。
理解内存性能
假设有以下函数。
1 |
|
经过测试发现,当 src 和 dst
指向不同的内存地址时(write_read(&a[0], &a[1], 3);
),运行效率相较于(write_read(&a[0], &a[0], 3);
)要快得多。
读写相关
读写相关:一个内存读的结果依赖于一个最近写的内存,为了确保读取结果的正确性,CPU需要额外的处理步骤,这可能导致流水线停顿并影响性能。
存储缓冲区的作用:CPU中存在一个存储缓冲区,其中存储着被发射到存储单元但还没完成的数据地址和数据内容,目的是隐藏写操作的延迟,允许处理器在存储操作实际完成之前继续执行其他指令。同时存储缓冲区能够临时保存写操作,合并对一个值的多次改写以减少对内存的实际写入。
存储缓冲区的运行机制
缓冲区运行时,创建条目和设置条目数据是不一定连续进行的,CPU无需等待设置数据后再进行其他指令。
- 创建条目:完成目标地址计算后,立即在存储缓冲区中创建条目。
- 设置数据:当计算完成或者数据读取完成后,设置数据值。
这种设计避免了流水线停顿,使得CPU在等待数据生成的同时可以继续执行其他指令。
存储缓冲区对读取操作的影响:但这也会导致当进行读取操作时,必须先检查被读取的数据在存储缓冲区之中是否存在,如果存在,则直接读取缓冲区中的最新数据,避免读取存储单元中的旧数据。
两个地址不同:后来的数据读取操作在检查缓冲区不存在待读取数据后便可直接执行读取数据的指令。
两个地址相同:后续的数据读取指令需要等待存储缓冲区中的条目数据填充完成后才能执行,也就是出现了数据相关性。这导致CPU无法充分利用乱序执行和流水线并行来加速操作。