我正在使用Kip Irvine的“x86处理器的汇编语言,第六版”,我真的非常喜欢它。
我刚刚阅读了以下段落中的NOP助记符:
"It [NOP] is sometimes used by compilers and assemblers to align code to
even-address boundaries."
给出的例子是:
00000000 66 8B C3 mov ax, bx
00000003 90 nop
00000004 8B D1 mov edx, ecx
然后该书指出:
"x86 processors are designed to load code and data more quickly from even
doubleword addresses."
我的问题是:原因是因为对于本书所指的x86处理器(32位),CPU的字大小为32位,因此它可以在NOP中提取指令并处理他们一气呵成?如果是这种情况,我假设一个字长为四字的64位处理器会用一个假设的5字节代码和一个nop来做这个吗?
最后,在我编写代码之后,我是否应该通过NOP进行正确的对齐以优化它,或者编译器(MASM,在我的情况下)是否会为我执行此操作,正如文本似乎暗示的那样?
谢谢,
斯科特
答案 0 :(得分:18)
在word(对于8086)或DWORD(80386及更高版本)边界上执行的代码执行得更快,因为处理器获取整个(D)字。因此,如果您的说明没有对齐,则加载时会出现停顿。
但是,你不能对每一条指令进行双调。好吧,我想你可以,但是那时你会浪费空间而且处理器必须执行NOP指令,这会损害对齐指令的任何性能优势。
实际上,在dword(或其他)边界上对齐代码仅在指令是分支指令的目标时才有用,并且编译器通常会对齐函数的第一条指令,但不会对齐也可以对齐的分支目标通过跌倒到达。例如:
MyFunction:
cmp ax, bx
jnz NotEqual
; ... some code here
NotEqual:
; ... more stuff here
生成此代码的编译器通常会对齐MyFunction
,因为它是一个分支目标(由call
到达),但它不会与NotEqual
对齐,因为这样做会插入NOP
坠落时必须执行的{{1}}指令。这会增加代码大小,并使落空情况变慢。
我建议如果您只是学习汇编语言,那么您不必担心这样的事情,这通常会给您带来微不足道的性能提升。只需编写代码即可使工作正常。在它们工作之后,您可以对它们进行分析,如果您认为在查看配置文件数据后有必要,请调整您的功能。
汇编程序通常不会自动为您执行此操作。
答案 1 :(得分:4)
因为(16位)处理器只能在偶数地址从内存中获取值,所以由于其特殊的布局:它被分成两个“库”,每个1个字节,所以数据总线的一半连接到第一个银行和另一半到另一家银行。现在,假设这些存储区已对齐(如我的图片所示),处理器可以获取位于同一“行”的值。
bank 1 bank 2
+--------+--------+
| 8 bit | 8 bit |
+--------+--------+
| | |
+--------+--------+
| 4 | 5 | <-- the CPU can fetch only values on the same "row"
+--------+--------+
| 2 | 3 |
+--------+--------+
| 0 | 1 |
+--------+--------+
\ / \ /
| | | |
| | | |
data bus (to uP)
现在,由于此获取限制,如果cpu被强制获取位于奇数地址的值(假设为3),则必须获取2和3处的值,然后取值为4和5,丢弃值2和5然后加入4和3(你在谈论x86,它作为一个小端存储器布局) 这就是为什么在偶数地址上更好地拥有代码(和数据!)的原因。
PS:在32位处理器上,代码和数据应该在可被4整除的地址上对齐(因为有4个存储区)。
希望我很清楚。 :)
答案 2 :(得分:1)
问题不仅限于取指令。令人遗憾的是,程序员并没有及早意识到这一点并经常受到惩罚。 x86架构让人们变得懒散。转换到其他架构时很难。
它与数据总线的性质有关。例如,当您具有32位宽的数据总线时,内存读取将在该边界上对齐。在这种情况下,通常忽略较低的两个地址位,因为它们没有意义。因此,如果您要从地址0x02执行32位读取,则它是指令获取或内存读取的一部分。然后需要两个存储器周期,从地址0x00读取以获得两个字节,从0x04读取以获得另外两个字节。如果这是取指令,则需要两倍的时间来停止管道。性能受到了巨大影响,绝不会浪费数据读取优化。将数据与自然边界对齐并以这些大小的整数倍调整结构和其他项目的程序可以看到性能的两倍,而无需任何其他努力。类似地,对于变量使用int而不是char,即使它只计算到10也可以更快。确实,将nops添加到程序以对齐分支目标通常是不值得的。不幸的是,x86是可变字长,基于字节,你经常遭受这些低效率。如果你被绘制成一个角落并且需要从循环中挤出更多的时钟,那么你不仅应该在与总线大小匹配的边界上(现在是32位或64位)而且还要在高速缓存行边界上对齐,并且尝试将该循环保持在一个或两个缓存行中。在该注释中,程序中的单个随机nop可以导致高速缓存行命中的变化,并且如果程序足够大并且具有足够的功能或循环,则可以检测到性能变化。同样的情况,例如,你在地址0xFFFC有一个分支目标,如果不在缓存中,必须获取缓存行,没有任何意外,但稍后需要一个或两个指令(四个字节)另一个缓存行。如果目标是0x10000,根据您的功能大小,您可能已经在一个缓存行中将其关闭。如果这是一个经常被调用的函数而另一个经常被称为函数的地址类似,那么这两个函数会相互驱逐,那么你的运行速度会慢两倍。这是一个x86帮助的地方,虽然可变指令长度,你可以将更多代码打包到缓存行,而不是其他使用良好的架构。
使用x86和指令提取你真的不能赢。在这一点上,尝试手动调整x86程序(从指令角度来看)通常是徒劳的。不同核心的数量及其细微差别,你可以在一台计算机上的一个处理器上获得收益,但是相同的代码会使其他计算机上的其他x86处理器运行速度变慢,有时速度不到一半。最好是普遍有效,但有一点草率让它每天在所有计算机上运行正常。数据对齐将显示跨计算机的处理器之间的改进,但指令对齐不会。