gpt4 book ai didi

performance - 可以使用哪些方法在现代x86上有效地扩展指令长度?

转载 作者:行者123 更新时间:2023-12-03 12:19:22 25 4
gpt4 key购买 nike

假设您想将一系列x86汇编指令与某些边界对齐。例如,您可能希望将循环对齐到16或32字节边界,或打包指令,以便将其有效地放置在uop缓存或任何其他内容中。

实现此目的的最简单方法是单字节NOP指令,其后紧跟multi-byte NOPs。尽管后者通常更有效,但是两种方法都不是免费的:NOP使用前端执行资源,并且也计入现代x86上的4-wide1重命名限制。

另一种选择是以某种方式延长一些说明以获得所需的对齐方式。如果做到这一点而又不引入新的拖延,那似乎比NOP方法更好。如何在最近的x86 CPU上有效地延长指令时间?

在理想情况下,加长技术将同时是:

  • 适用于大多数说明
  • 能够将指令延长可变数量
  • 不会停止,否则会降低解码器的速度
  • 在uop缓存中有效表示

  • 不可能有一种方法可以同时满足上述所有要点,因此好的答案可能会解决各种折衷问题。

    1AMD Ryzen的限制为5或6。

    最佳答案

    考虑使用温和的代码高尔夫来收缩代码,而不是来扩展代码,尤其是在循环之前。例如xor eax,eax / cdq(如果需要两个清零寄存器),或者mov eax, 1 / lea ecx, [rax+1]将寄存器设置为1和2(仅8个字节)而不是10个字节。有关更多信息,请参见Set all bits in CPU register to 1 efficiently;有关更一般的想法,请参见Tips for golfing in x86/x64 machine code。不过,也许您仍然想避免错误的依赖关系。

    或通过creating a vector constant on the fly 填充多余的空间,而不是从内存中加载它。 (不过,对于包含设置+内部循环的较大循环,增加更多的uop缓存压力可能会更糟。但是,它避免了d缓存丢失常量的情况,因此它具有弥补运行更多uops的优势。)

    如果尚未使用它们来加载“压缩”常量,则pmovsxbdmovddupvpbroadcastd的长度比movaps长。 dword / qword广播加载是免费的(没有ALU uop,只有负载)。

    如果您完全担心代码对齐,那么您可能会担心它在L1I缓存中的位置或uop缓存边界的位置,因此仅计算总uops就不再足够了,并且在其中增加了一些uops在您关心的对象之前阻止可能根本不是问题。

    但是在某些情况下,您可能确实想要针对要对齐的块之前的指令优化解码吞吐量/ uop-cache使用率/总uops。

    填充说明,例如要求的问题:

    Agner Fog在这方面有一整节内容:“"Optimizing subroutines in assembly language" guide”中的是“10.6延长指令以便对齐”。 (leapush r/m64和SIB的想法都来自那里,我复制了一两个句子/短语,否则,这个答案是我自己的工作,或者是不同的想法,或者是在查阅Agner指南之前写的。)

    但是,当前的CPU尚未对其进行更新:lea eax, [rbx + dword 0]的缺点要比mov eax, ebx多,因为您错过了zero-latency / no execution unit mov 。如果它不在关键路径上,请继续努力。简单的lea具有相当好的吞吐量,而具有较大寻址模式(甚至可能有一些段前缀)的LEA可以比mov + nop更好地解码/执行吞吐量。

    使用一般形式,而不是push regmov reg,imm之类的简短说明(无ModR / M)。例如使用2字节push r/m64作为push rbx。或者使用更长的等效指令,例如add dst, 1而不是inc dstin cases where there are no perf downsides to inc ,因此您已经在使用inc

    使用SIB字节。您可以通过使用单个寄存器作为索引来使NASM做到这一点,例如mov eax, [nosplit rbx*1](see also),但是与仅使用SIB字节编码mov eax, [rbx]相比,这会损害负载使用延迟。索引寻址模式在SnB系列like un-lamination and not using port7 for stores上还有其他缺点。

    因此,最好只使用不带索引reg 的ModR / M + SIB来编码base=rbx + disp0/8/32=0。 (“无索引”的SIB编码是否则将意味着idx = RSP的编码)。 [rsp + x]寻址模式已经需要一个SIB(base = RSP是转义码,表示有一个SIB),并且始终在编译器生成的代码中出现。因此,有充分的理由期望现在和将来,这种方法在解码和执行(甚至对于RSP以外的基本寄存器)方面都将是完全有效的。 NASM语法无法表达这一点,因此您必须手动编码。 objdump -d中的GNU gas Intel语法对Agner Fog的示例10.20表示8b 04 23 mov eax,DWORD PTR [rbx+riz*1]。 (riz是一种虚构的索引零符号,表示存在没有索引的SIB)。我尚未测试GAS是否接受该输入。

    使用仅需要imm32disp32的指令的imm8和/或disp0/disp32形式。 Agner Fog对Sandybridge的uop缓存(microarch guide table 9.1)的测试表明,立即数/位移的实际值很重要,而不是指令编码中使用的字节数。我没有有关Ryzen的uop缓存的任何信息。

    因此,NASM imul eax, [dword 4 + rdi], strict dword 13(10个字节:操作码+ modrm + disp32 + imm32)将使用32small,32small类别并在uop缓存中占用1个条目,这与立即数或disp32实际上有16个以上有效位不同。 (这将需要2个条目,并且从uop缓存中加载它会花费一个额外的周期。)

    根据Agner的表格,SnB始终等于8/16 / 32small。寄存器的寻址方式是完全相同的,无论是否没有位移,或者它是否都是32small,因此mov dword [dword 0 + rdi], 123456需要2个条目,就像mov dword [rdi], 123456789一样。我还没有意识到[rdi] +完整的imm32接受了2个条目,但是显然在SnB上就是这种情况。

    使用jmp / jcc rel32代替rel8 。理想情况下,尝试在不需要扩展编码的地方扩展指令。 在跳转目标之后填充以进行较早的向前跳转,在跳转目标之前填充以进行较后的向后跳转,(如果它们接近在其他地方需要rel32)。即尝试避免分支和目标之间的填充,除非您仍然希望该分支使用rel32。

    您可能会想通过使用地址大小前缀使用32位绝对地址来将mov eax, [symbol]编码为64位代码中的6字节a32 mov eax, [abs symbol]。但是this does cause a Length-Changing-Prefix stall在Intel CPU上解码时。幸运的是,如果您未显式指定32位地址大小,而是使用7字节mov r32, r/m32和ModR / M + SIB + disp32绝对值,则默认情况下,没有NASM / YASM / gas / clang会执行此代码大小优化mov eax, [abs symbol]的寻址模式。

    在64位位置相关的代码中,相对于RIP相对的,绝对寻址是一种便宜的使用1个额外字节的方式。但是请注意,与RIP相对+ imm8 / 16/32相比,即使它仍使用2条指令,它也需要2个周期才能从uop缓存中获取,而不是RIP相对+ imm8 / 16/32。 (例如mov -store或cmp)。因此,即使两者都各自占用2个条目,从utop缓存中获取cmp [abs symbol], 123的速度也比cmp [rel symbol], 123慢。没有立即的服务,就不会有额外的费用

    请注意,即使可执行文件and are the default in many Linux distro,PIE可执行文件也允许使用ASLR,因此,如果可以保留代码PIC而没有任何不利影响,那么那是更好的选择。

    不需要时使用REX前缀,例如db 0x40 / add eax, ecx

    通常不安全地添加当前CPU忽略的诸如rep之类的前缀,因为在将来的ISA扩展中它们可能意味着其他含义。

    有时可能会重复相同的前缀(不过REX不能)。例如,db 0x66, 0x66 / add ax, bx给出指令3个操作数大小的前缀,我认为它始终严格等同于该前缀的一个副本。在某些CPU上,最多3个前缀是有效解码的限制。但这仅在您具有可以首先使用的前缀的情况下有效;您通常不使用16位操作数大小,并且通常不希望使用32位地址大小(尽管使用位置相关代码访问静态数据是安全的)。

    存取内存的指令上的dsss前缀是no-op ,并且可能不会导致任何当前CPU的速度下降。 (@prl在评论中建议了这一点)。

    实际上, Agner Fog的微体系结构指南在示例7.1中在ds上使用了movq
    [esi+ecx],mm0
    前缀。安排IFETCH块
    可以调整PII / PIII的循环(无循环缓冲区或uop缓存),将其从每个时钟3次迭代加速到2次。

    当指令的前缀超过3个时,某些CPU(如AMD)会缓慢解码。在某些CPU上,这包括SSE2中的强制前缀,尤其是SSSE3 / SSE4.1指令中的强制前缀。在Silvermont中,即使0F转义字节也很重要。

    AVX指令可以使用2字节或3字节的VEX前缀。有些指令需要3字节的VEX前缀(第二个源是x / ymm8-15,或者SSSE3或更高版本的强制前缀)。但是,可以使用2字节前缀的指令始终可以使用3字节VEX进行编码。 NASM或GAS {vex3} vxorps xmm0,xmm0。如果有AVX512,则也可以使用4字节EVEX。

    即使不需要使用mov,也请使用64位操作数大小的,例如mov rax, strict dword 1在NASM which would normally optimize it to 5-byte mov eax, 1 中强制使用7字节的符号扩展imm32编码。

    mov    eax, 1                ; 5 bytes to encode (B8 imm32)
    mov rax, strict dword 1 ; 7 bytes: REX mov r/m64, sign-extended-imm32.
    mov rax, strict qword 1 ; 10 bytes to encode (REX B8 imm64). movabs mnemonic for AT&T.

    您甚至可以使用 mov reg, 0代替 xor reg,reg

    当常量实际上很小(适合32位符号扩展)时, mov r64, imm64有效地适合uop缓存。 1个uop缓存条目,并且load-time = 1,与 mov r32, imm32相同。解码一条巨型指令意味着在16字节解码块中可能没有空间在同一周期内解码其他3条指令,除非它们全为2字节。可能稍微延长多条其他指令可能比拥有一条长指令更好。

    解码额外前缀的惩罚:
  • P5:前缀阻止配对,只有PMMX上的地址/操作数大小除外。
  • PPro到PIII:如果一条指令具有多个前缀,则始终会受到惩罚。每个额外的前缀通常要花费一个时钟。 (Agner的Microarch指南,第6.3节的结尾)
  • Silvermont:如果您关心的话,这可能是对可以使用的前缀的最严格的限制。解码停滞在3个以上的前缀上,计数强制前缀+ 0F转义字节。 SSSE3和SSE4指令已经具有3个前缀,因此,即使REX也会使它们的解码速度变慢。
  • 一些AMD:可能是3个前缀的限制,不包括转义字节,并且可能不包括SSE指令的强制前缀。

  • ... TODO:完成本节。在此之前,请查阅Agner Fog的微体系结构指南。

    对内容进行手工编码后,请始终对二进制文件进行反汇编,以确保正确使用。不幸的是,NASM和其他汇编程序没有更好的支持,无法在指令区域上选择便宜的填充来达到给定的对齐边界。

    汇编语法

    NASM具有一些编码替代语法: {vex3}{evex}前缀, NOSPLITstrict byte / dword,并在寻址模式中强制使用disp8 / disp32。请注意,不允许使用 [rdi + byte 0],必须首先使用 byte关键字。 [byte rdi + 0]是允许的,但是我认为这很奇怪。

    list 来自 nasm -l/dev/stdout -felf64 padding.asm
     line  addr    machine-code bytes      source line
    num

    4 00000000 0F57C0 xorps xmm0,xmm0 ; SSE1 *ps instructions are 1-byte shorter
    5 00000003 660FEFC0 pxor xmm0,xmm0
    6
    7 00000007 C5F058DA vaddps xmm3, xmm1,xmm2
    8 0000000B C4E17058DA {vex3} vaddps xmm3, xmm1,xmm2
    9 00000010 62F1740858DA {evex} vaddps xmm3, xmm1,xmm2
    10
    11
    12 00000016 FFC0 inc eax
    13 00000018 83C001 add eax, 1
    14 0000001B 4883C001 add rax, 1
    15 0000001F 678D4001 lea eax, [eax+1] ; runs on fewer ports and doesn't set flags
    16 00000023 67488D4001 lea rax, [eax+1] ; address-size and REX.W
    17 00000028 0501000000 add eax, strict dword 1 ; using the EAX-only encoding with no ModR/M
    18 0000002D 81C001000000 db 0x81, 0xC0, 1,0,0,0 ; add eax,0x1 using the ModR/M imm32 encoding
    19 00000033 81C101000000 add ecx, strict dword 1 ; non-eax must use the ModR/M encoding
    20 00000039 4881C101000000 add rcx, strict qword 1 ; YASM requires strict dword for the immediate, because it's still 32b
    21 00000040 67488D8001000000 lea rax, [dword eax+1]
    22
    23
    24 00000048 8B07 mov eax, [rdi]
    25 0000004A 8B4700 mov eax, [byte 0 + rdi]
    26 0000004D 3E8B4700 mov eax, [ds: byte 0 + rdi]
    26 ****************** warning: ds segment base generated, but will be ignored in 64-bit mode
    27 00000051 8B8700000000 mov eax, [dword 0 + rdi]
    28 00000057 8B043D00000000 mov eax, [NOSPLIT dword 0 + rdi*1] ; 1c extra latency on SnB-family for non-simple addressing mode

    GAS包含encoding-override pseudo-prefixes {vex3}{evex}{disp8}{disp32} These replace the now-deprecated .s , .d8 and .d32 suffixes

    GAS并没有覆盖立即尺寸,只有位移。

    GAS允许您使用 ds添加一个显式的 ds mov src,dst前缀
    gcc -g -c padding.S && objdump -drwC padding.o -S,带有手动编辑:
      # no CPUs have separate ps vs. pd domains, so there's no penalty for mixing ps and pd loads/shuffles
    0: 0f 28 07 movaps (%rdi),%xmm0
    3: 66 0f 28 07 movapd (%rdi),%xmm0

    7: 0f 58 c8 addps %xmm0,%xmm1 # not equivalent for SSE/AVX transitions, but sometimes safe to mix with AVX-128

    a: c5 e8 58 d9 vaddps %xmm1,%xmm2, %xmm3 # default {vex2}
    e: c4 e1 68 58 d9 {vex3} vaddps %xmm1,%xmm2, %xmm3
    13: 62 f1 6c 08 58 d9 {evex} vaddps %xmm1,%xmm2, %xmm3

    19: ff c0 inc %eax
    1b: 83 c0 01 add $0x1,%eax
    1e: 48 83 c0 01 add $0x1,%rax
    22: 67 8d 40 01 lea 1(%eax), %eax # runs on fewer ports and doesn't set flags
    26: 67 48 8d 40 01 lea 1(%eax), %rax # address-size and REX
    # no equivalent for add eax, strict dword 1 # no-ModR/M

    .byte 0x81, 0xC0; .long 1 # add eax,0x1 using the ModR/M imm32 encoding
    2b: 81 c0 01 00 00 00 add $0x1,%eax # manually encoded
    31: 81 c1 d2 04 00 00 add $0x4d2,%ecx # large immediate, can't get GAS to encode this way with $1 other than doing it manually

    37: 67 8d 80 01 00 00 00 {disp32} lea 1(%eax), %eax
    3e: 67 48 8d 80 01 00 00 00 {disp32} lea 1(%eax), %rax


    mov 0(%rdi), %eax # the 0 optimizes away
    46: 8b 07 mov (%rdi),%eax
    {disp8} mov (%rdi), %eax # adds a disp8 even if you omit the 0
    48: 8b 47 00 mov 0x0(%rdi),%eax
    {disp8} ds mov (%rdi), %eax # with a DS prefix
    4b: 3e 8b 47 00 mov %ds:0x0(%rdi),%eax
    {disp32} mov (%rdi), %eax
    4f: 8b 87 00 00 00 00 mov 0x0(%rdi),%eax
    {disp32} mov 0(,%rdi,1), %eax # 1c extra latency on SnB-family for non-simple addressing mode
    55: 8b 04 3d 00 00 00 00 mov 0x0(,%rdi,1),%eax

    对于表示比需要的更长的编码,GAS的功能严格上不到NASM。

    关于performance - 可以使用哪些方法在现代x86上有效地扩展指令长度?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48046814/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com