ayu*_*hgp 4 c language-agnostic x86 assembly pipeline
当我们编译代码并执行它时,在汇编中,我们的代码被转换,函数以非顺序方式存储.因此,每次调用函数时,处理器都需要丢弃管道中的指令.这不会影响程序的性能吗?
PS:我没有考虑投入时间来开发没有功能的程序.纯粹在性能水平上.编译器是否有任何方法可以解决这个问题?
因此,每次调用函数时,处理器都需要丢弃管道中的指令.
不,解码阶段后的一切仍然很好.该CPU不知道保持解码无条件转移后(如jmp
,call
或ret
).只有已经提取但尚未解码的指令才是不应运行的指令.在从指令解码目标地址之前,对于管道的开始没有任何用处,因此在管道中出现气泡直到目标地址已知.尽可能早地解码分支指令因此最小化了所采用分支的惩罚.
在经典的RISC流水线中,阶段是IF ID EX MEM WB
(获取,解码,执行,mem,回写(结果到寄存器).因此,当ID解码分支指令时,管道抛弃当前在IF中获取的指令,并且指令当前正在ID中解码(因为它是分支后的指令).
"危险"是指阻止稳定的指令流每个时钟通过管道的事物.分支是控制危害.(控制与流量控制相反,而不是数据.)
如果分支目标不在L1 I-cache中,则管道将必须等待指令从存储器流入,然后IF流水线阶段才能产生取出的指令. I-cache misses总是会产生管道泡沫.对于非分支代码,预取通常会避免这种情况.
更复杂的CPU解码得足以检测分支并很快重新引导提取以隐藏此气泡.这可能涉及解码指令队列以隐藏获取气泡.
而且,CPU不是实际解码以检测分支指令,而是可以针对"分支目标缓冲区"高速缓存检查每个指令地址.如果你受到了打击,即使你还没有解码它,你知道该指令是一个分支.BTB还保存目标地址,因此您可以立即从那里开始提取(如果它是无条件分支,或者您的CPU支持基于分支预测的推测执行).
ret
实际上是更难的情况:返回地址在寄存器或堆栈中,不直接编码到指令中.这是一个无条件的间接分支.现代x86 CPU维护内部返回地址预测器堆栈,并且当您不正确地调用call/ret指令时执行非常糟糕.例如call label
/ label: pop ebx
对于与位置无关的32位代码来说,将EIP引入EBX是非常糟糕的.这将导致ret
对调用树的下一个15左右的错误预测.
我想我已经读过一些其他非x86微体系结构使用了返回地址预测器堆栈.
请参阅Agner Fog的微体系结构pdf,以了解有关x86 CPU行为的更多信息(另请参阅x86标签wiki),或阅读计算机体系结构教科书以了解简单的RISC管道.
有关缓存和内存的更多信息(主要关注数据缓存/预取),请参阅Ulrich Drepper的"每个程序员应该了解的关于内存的内容".
无条件分支非常便宜,比如通常情况下最差的几个周期(不包括I-cache未命中).
函数调用的巨大代价是当编译器无法看到目标函数的定义时,必须假设它在调用约定中使用了所有调用符号寄存器.(在x86-64 SystemV中,所有浮点/向量寄存器和大约8个整数寄存器.)这需要溢出到内存或将实时数据保存在调用保留寄存器中.但这意味着该函数必须保存/恢复这些寄存器以不破坏调用者.
程序间优化让函数利用知道哪些寄存器实际上是破坏的,哪些不是,这是编译器可以在同一个编译单元中完成的事情.甚至跨编译单元进行链接时整个程序优化.但它不能扩展到动态链接边界,因为不允许编译器生成会破坏相同共享库的不同编译版本的代码.
编译器是否有任何方法可以解决这个问题?
它们内联小函数,甚至static
只调用一次的大函数.
int foo(void) { return 1; }
mov eax, 1 #,
ret
int bar(int x) { return foo() + x;}
lea eax, [rdi+1] # D.2839,
ret
Run Code Online (Sandbox Code Playgroud)
正如@harold指出的那样,过度使用内联可能会导致缓存未命中,因为它会大大增加代码大小,以至于并非所有热代码都适合缓存.
英特尔SnB系列设计具有小但非常快的uop缓存,可缓存已解码的指令.它最多只能支持1536 uops IIRC,每行6个uop.从uop缓存而不是解码器执行会将分支错误预测惩罚从19个缩短为15个循环,IIRC(类似的东西,但这些数字可能对任何特定的uarch都不正确).与解码器相比,还有一个显着的前端吞吐量提升,尤其是.对于矢量代码中常见的长指令.