网站首页 > 资源文章 正文
这是汇编部分的最后一章,可能大部分的程序员现阶段已经接触不到汇编部分了,但是了解一下底层部分,还是可以帮助我们对整个的计算机体系结构有一个更清楚的认知的。
机器码布局(Machine Code Layout)
程序员们喜欢在将CPU的流水线划分为两部分:前端和后端,前段指的是指令从内存中获取并解码的部分,后端代表指令被调度并最终执行的部分。
通常,性能受到执行阶段的限制,因此,我们大部分的努力将用于围绕后端进行优化。
但有时候,相反的情况也会发生,比如前端部分无法足够快地向后端提供指令来饱和它。这可能由许多原因造成,所有这些原因最终都与机器码在内存中的布局有关,这些原因会以奇怪的方式影响程序性能。移除未使用的代码、交换“if”分支,甚至改变函数声明的顺序都有可能会导致性能的提高或恶化。
CPU前端(CPU Front-End)
在机器码被转换为指令和让CPU理解程序员想要做什么之前,CPU首先需要经历的两个重要阶段:获取和解码。
在获取阶段,CPU简单地从主内存中加载固定大小的字节块,这些块中包含指令的二进制编码。在x86结构的CPU上,这个块的大小通常是32字节,在不同架构的机器上可能会有所不同。一个很重要的细节就是这个块必须对齐:意味着块的地址必须是其大小的倍数(在我们的例子中是32B)。
接下来是解码阶段:CPU查看这些字节块,丢弃指令指针之前的内容,并将剩下的内容分割成指令。机器指令使用可变数量的字节进行编码:像inc rax这样简单且非常常见的指令占用一个字节,而某些带有编码常量和行为前缀的不常见指令,可能占用多达15个字节。因此,从32字节块中可能解码出可变数量的指令,但不会超过特定于机器的限制,称为解码宽度。在我的CPU(Zen 2)上,解码宽度是4,这意味着在每个周期中,最多可以解码4条指令并传递到下一个阶段。
这些阶段以流水线方式工作:如果CPU能够预测(或预测)它接下来需要哪个指令块,那么获取阶段就不会等待当前块中的最后一条指令被解码,而是立即加载下一个块。
代码对齐
当其他条件相同时,编译器通常更喜欢较短机器码的指令,因为这样可以在单个32B的获取块中放入更多的指令,同时也会减少二进制文件的大小。但有时可能反过来更好,因为获取到的指令块必须对齐。
比如,我们需要执行一个从32B对齐块的最后一个字节开始的指令序列。可能能够在不需要额外延迟的情况下执行第一条指令,但对于后续指令,我们必须等待一个额外的周期来执行另一个指令获取。如果代码块在32B边界上对齐,那么最多可以同时解码并执行4条指令(除非它们特别长或相互依赖)。
考虑到这一点,编译器有时会执行看似多余的优化:它们有时更喜欢具有较长机器码的指令,甚至插入不做任何事情的虚拟指令1,以便将关键跳转位置对齐到合适的边界上。
在GCC中,可以使用-falign-labels=n标志来指定特定的对齐策略,如果想更具选择性,可以用 -function、-loops 或 -jumps 替换 -labels。在开启 -O2 和 -O3 优化级别时,会默认启用不设置特定对齐的策略,这种情况下编译器会使用(通常合理的)特定于机器的默认值。
指令缓存
指令的存储和获取的内存系统,和数据内存系统大致相同,除了用单独的指令缓存替换掉较低层的缓存(因为我们不希望随机的数据读取将处理它的指令代码挤掉)。
在以下情况下,指令缓存至关重要:
- 我们不知道接下来要执行哪些指令,并需要以低延迟获取下一个字节块,
- 正在执行一长串冗长但快速处理的指令,并需要较高的带宽。
因此,对于具有大量机器码的程序,内存系统可能成为瓶颈。这种情况限制了我们之前讨论的一些典型优化技术的适用性:
- 内联函数并不总是最佳的,因为它降低了代码共享并增加了二进制文件的大小,需要更多的指令缓存。
- 展开循环只在一定程度上有益,即使在编译时已知迭代次数:在某个时刻,CPU必须从主内存中获取指令和数据,在这种情况下,它很可能受到内存带宽的限制。
- 大量的代码对齐增加了二进制文件的大小,同样需要更多的指令缓存。与缺失缓存并等待指令从主内存中获取相比,多花一个周期进行指令获取是一个较小的代价。
另一个方法是,把频繁使用的指令序列放在同一缓存行和内存页面上,这样可以提高缓存局部性。为了提高指令缓存的利用率,应该将热代码与热代码组合在一起,将冷代码与冷代码组合在一起,并尽可能删除死代码(未使用的代码)。如果想进一步探索这个想法,请查看Facebook的二进制优化和布局工具(Binary Optimization and Layout Tool),该工具最近已经被合并到LLVM中。
不均等分支(Unequal Branches)
我们来看下面一个例子,我们需要一个计算整数区间长度的辅助函数。它接受两个参数,x和y,但为了方便,这个区间可能是[x,y],或[y,x],取决于x和y的大小。在C语言中,我们可能会像下面这样写:
int length(int x, int y) {
if (x > y)
return x - y;
else
return y - x;
}
在x86架构的汇编中,实现它的方式有很多种,会显著影响性能。让我们从尝试将这段代码直接映射到汇编开始:
length:
cmp edi, esi
jle less
; x > y
sub edi, esi
mov eax, edi
done:
ret
less:
; x <= y
sub esi, edi
mov eax, esi
jmp done
尽管初始的C代码中,if和else的两个执行分支看起来非常对称,汇编版本却不是。这导致了一个有趣的点,即一个分支会比另一个稍微快一点执行:如果 x > y,则CPU只需要执行 cmp 和 ret 之间的5条指令,如果函数对齐,这些指令都会一次性获取;而在 x <= y 的情况下,需要两次额外的跳转。
如果假设x > y 的情况不太可能发生(为什么会有人计算反向区间的长度呢?),更像是几乎从不发生的异常。我们可以检测这种情况,并简单地交换 x 和 y:
int length(int x, int y) {
if (x > y)
swap(x, y);
return y - x;
}
汇编代码将如下进行,就像通常对于没有else的if模式一样:
length:
cmp edi, esi
jle normal ; 如果 x <= y,不需要交换,我们可以跳过 xchg
xchg edi, esi
normal:
sub esi, edi
mov eax, esi
ret
现在指令总长度是6,从8减少。但它仍然没有为我们假设的情况完全优化:如果我们认为 x > y 从不发生,那么在加载从不执行的 xchg edi, esi 指令时就是在浪费。我们可以通过将其移到正常执行路径之外来解决这个问题:
length:
cmp edi, esi
jg swap
normal:
sub esi, edi
mov eax, esi
ret
swap:
xchg edi, esi
jmp normal
这种技术在一般处理异常情况时非常方便,在高级代码中,我们可以给编译器一个提示,表明某个分支比另一个更可能:
int length(int x, int y) {
if (x > y) [[unlikely]]
swap(x, y);
return y - x;
}
当我们知道一个分支很少被采用时,这种优化才有益。不是这种情况时,有很多比代码布局更好的方法来促使编译器避免任何分支——在这种情况下,通过使用特殊的“条件移动”指令替换它,大致对应于三元表达式 (x > y ? y - x : x - y) 或调用 abs(x - y):
length:
mov edx, edi
mov eax, esi
sub edx, esi
sub eax, edi
cmp edi, esi
cmovg eax, edx ; "mov if edi > esi"
ret
消除分支也是一个相当重要的话题,我们将后面的文章中更详细地讨论它。
猜你喜欢
- 2024-10-04 汇编语言真的没必要学了吗!(汇编语言值得学吗)
- 2024-10-04 Adobe各种软件问题解决汇编(adobe程序)
- 2024-10-04 最基础的逆向实战教程(最好的逆向教程)
- 2024-10-04 ida汇编逆向游戏脚本网络协议逆向插件海外脱机27
- 2024-10-04 汇编语言入门(一)(汇编语言简明教程)
- 2024-10-04 X86 架构处理器汇编伪指令介绍(x86汇编指令集解析)
- 2024-10-04 玩一玩avr汇编,探究计算机的本质
- 2024-10-04 学习汇编语言有什么用?怎么才能学好?
- 2024-10-04 Windows11搭建汇编开发环境(汇编开发环境配置)
- 2024-10-04 管理必备:57页企业发展战略常用工具汇编(PPT完整版)
你 发表评论:
欢迎- 最近发表
- 标签列表
-
- 电脑显示器花屏 (79)
- 403 forbidden (65)
- linux怎么查看系统版本 (54)
- 补码运算 (63)
- 缓存服务器 (61)
- 定时重启 (59)
- plsql developer (73)
- 对话框打开时命令无法执行 (61)
- excel数据透视表 (72)
- oracle认证 (56)
- 网页不能复制 (84)
- photoshop外挂滤镜 (58)
- 网页无法复制粘贴 (55)
- vmware workstation 7 1 3 (78)
- jdk 64位下载 (65)
- phpstudy 2013 (66)
- 卡通形象生成 (55)
- psd模板免费下载 (67)
- shift (58)
- localhost打不开 (58)
- 检测代理服务器设置 (55)
- frequency (66)
- indesign教程 (55)
- 运行命令大全 (61)
- ping exe (64)
本文暂时没有评论,来添加一个吧(●'◡'●)