assembly 为什么代码要与x86上的偶数地址边界对齐?

l2osamch  于 2022-12-13  发布在  其他
关注(0)|答案(3)|浏览(121)

我正在学习Kip Irvine的**“第六版x86处理器汇编语言”**,我真的很喜欢。
我刚刚在下面的段落中读到了关于NOP助记符的内容:
编译器和汇编器有时会使用它[NOP]将代码与偶数地址边界对齐。”
给出的示例为:

00000000   66 8B C3   mov ax, bx
00000003   90         nop
00000004   8B D1      mov edx, ecx

书中接着指出:
x86处理器的设计目的是更快地从偶数双字地址加载代码和数据。
我的问题是:这是不是因为对于本书提到的x86处理器(32位),CPU的字长是32位,因此它可以将带有NOP的指令拉入并一次性处理?如果是这样,我假设字长为四字的64位处理器可以使用假设的5字节代码加上一个NOP来完成这一任务?
最后,在我编写代码之后,我应该检查并纠正与NOP的对齐以优化它,还是编译器(在我的情况下是MASM)为我做这件事,正如文本似乎暗示的那样?

ufj5ltwl

ufj5ltwl1#

在字(对于8086)或DWORD(80386及更高版本)边界上执行的代码执行速度更快,因为处理器提取整个(D)字。因此,如果您的指令没有对齐,则在加载时会出现停顿。
然而,你不能对每条指令都进行双字对齐。我想你可以,但这样会浪费空间,处理器必须执行NOP指令,这会扼杀对齐指令带来的任何性能优势。
在实践中,只有当指令是分支指令的目标时,在dword(或其他)边界上对齐代码才有帮助,编译器通常会对齐函数的第一条指令,但不会对齐也可以通过fallthrough到达的分支目标。例如:

MyFunction:
    cmp ax, bx
    jnz NotEqual
    ; ... some code here
NotEqual:
    ; ... more stuff here

生成这段代码的编译器通常会对齐MyFunction,因为它是一个分支目标(由call到达),但它不会对齐NotEqual,因为这样做会插入NOP指令,这些指令在失败时必须执行,这会增加代码大小,并使失败情况变慢。
我建议,如果你只是在学习汇编语言,你不必担心这类通常会给你带来边际性能增益的事情。只需要编写代码来使它们工作。在它们工作后,你可以分析它们,如果你在看了分析数据后认为有必要的话,调整你的函数。
汇编程序通常不会自动为您执行此操作。

tvokkenx

tvokkenx2#

由于(16位)处理器的特殊布局,它只能从偶数地址的存储器中读取值:它被分成两个“存储体”,每个存储体1个字节,所以数据总线的一半连接到第一个存储体,另一半连接到另一个存储体。2现在,假设这些存储体是对齐的(如我的图片所示),处理器可以获取在同一“行”上的值。

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个存储体)。
希望我说清楚了。:)

ubof19bj

ubof19bj3#

这个问题不仅仅局限于指令获取。不幸的是,程序员没有及早意识到这一点,并经常因此受到惩罚。x86架构让人们变得懒惰。这使得向其他架构过渡变得困难。
这与数据总线的性质有关。例如,当数据总线为32位宽时,从存储器读取的数据将在该边界对齐。在这种情况下,较低的两个地址位通常会被忽略,因为它们没有意义。因此,如果要从地址0x02执行32位读取,则需要两个存储器周期,从地址0x00读取以获得两个字节,并且从0x04读取以获得另外两个字节。花费两倍的时间,如果这是一个指令获取,则暂停流水线。性能损失是显著的,并且决不是浪费对数据读取的优化。在自然边界上对齐数据并以这些大小的整数倍调整结构和其他项的程序,可以看到不需要任何其他努力就可以将性能提高多达一倍。类似地,对变量使用int而不是char,即使它只会计数到10也会更快。的确,在程序中添加nop来对齐分支目标通常不值得这么做。不幸的是,x86是可变字长的,基于字节,您经常会遇到这些低效率的问题。如果您被逼到了墙角,需要从循环中挤出更多的时钟,您不仅应该在与总线大小匹配的边界上对齐,(现在是32或64位)而且在高速缓存线边界上,并试图将循环保持在一个或可能两个高速缓存行内。在这一点上,程序中的单个随机nop可能导致该高速缓存行命中的位置发生变化,如果程序足够大并且具有足够多的函数或循环,则可以检测到性能变化。例如,假设在地址0xFFFC处有一个分支目标,如果该高速缓存中,则必须获取高速缓存行,没有意外,但在一个或两个指令之后如果目标是0x10000,那么根据函数的大小,如果这是一个经常被调用的函数,而另一个经常被调用的函数在一个足够相似的地址上,这两个函数会互相驱逐,你的运行速度会慢两倍。这是x86的一个优势,尽管与其他常用的体系结构相比,x86具有可变的指令长度,可以将更多的代码装入一个高速缓存行。
使用x86和指令获取,你不可能真正取胜。在这一点上,试图手动调整x86程序通常是徒劳的(从指令的Angular 来看)。不同内核的数量和它们的细微差别,你可以在一台电脑上的一个处理器上取得收益,但同样的代码会让其他电脑上的其他x86处理器运行得更慢,有时甚至不到一半的速度。2最好是有效率的,但要有一点马虎,让它每天在所有的计算机上都能正常运行。3数据对齐会在处理器和计算机之间显示出改进,但指令对齐不会。

相关问题