x86 / x64机器代码中打高尔夫球的技巧


27

我注意到没有这样的问题,所以这里是:

您是否有使用机器码打高尔夫球的一般技巧?如果提示仅适用于特定环境或调用约定,请在您的答案中进行指定。

每个答案请只提供一个小技巧(请参阅此处)。

Answers:


11

mov-立即数对于常量来说很昂贵

这可能很明显,但我仍将其放在此处。通常,在需要初始化值时,考虑数字的位级表示会很有意义。

初始化eax0

b8 00 00 00 00          mov    $0x0,%eax

应该缩短(出于性能以及代码大小的考虑)以

31 c0                   xor    %eax,%eax

初始化eax-1

b8 ff ff ff ff          mov    $-1,%eax

可以缩短为

31 c0                   xor    %eax,%eax
48                      dec    %eax

要么

83 c8 ff                or     $-1,%eax

或更一般而言,可以在3个字节push -12(2个字节)/ pop %eax(1个字节)中创建任何8位符号扩展值。 这甚至适用于没有额外REX前缀的64位寄存器。push/ pop默认操作数大小= 64。

6a f3                   pushq  $0xfffffffffffffff3
5d                      pop    %rbp

或在寄存器中给定已知常量,则可以使用lea 123(%eax), %ecx(3个字节)创建另一个附近的常量。如果您需要一个清零的寄存器一个常量,这将很方便。异或零(2个字节)+ lea-disp8(3个字节)。

31 c0                   xor    %eax,%eax
8d 48 0c                lea    0xc(%eax),%ecx

另请参见将CPU寄存器中的所有位有效设置为1


另外,要使用较小的(8位)值(而不是0)初始化寄存器:请使用push 200; pop edx-3个字节进行初始化。
anatolyg

2
BTW将寄存器初始化为-1,使用decxor eax, eax; dec eax
anatolyg

@anatolyg:200是一个很糟糕的例子,它不适合在sign-extended-imm8中使用。但是是的,push imm8/ pop reg是3个字节,对于x86-64上的64位常量非常有用,其中dec/ inc是2个字节。和push r64/ pop 64(2个字节)甚至可以替换一个3字节mov r64, r64(3个字节与REX)。另请参见设置在CPU寄存器中的所有位为1有效的一样的东西lea eax, [rcx-1]在给定一个已知值eax(例如,如果需要的归零寄存器另一个不变,只是使用LEA,而不是推/弹出
彼得·科德斯

10

在很多情况下,基于累加器的指令(即,那些(R|E)AX作为目标操作数的指令)比一般情况下的指令短1个字节。在StackOverflow上看到这个问题


通常,最有用的是al, imm8特殊的情况下,像or al, 0x20/ sub al, 'a'/ cmp al, 'z'-'a'/ ja .non_alphabetic为各2个字节,而不是3.使用al字符数据还允许lodsb和/或stosb。或使用al测试一些关于EAX的低字节,像lodsd/ test al, 1/ setnz cl使CL = 1或0用于奇数/偶数。但是在极少数情况下,您需要立即使用32位,然后确定op eax, imm32,就像我的色键回答
Peter Cordes

8

选择您的调用约定以将args放置在所需的位置。

答案的语言是asm(实际上是机器代码),因此应将其视为以asm编写的程序的一部分,而不是C-compiled-x86。 您的函数不必通过任何标准的调用约定就可以轻松地从C调用。但是,如果它不花费您任何额外的字节,那将是一个不错的奖励。

在纯asm程序中,某些帮助程序函数使用对他们和他们的调用者都方便的调用约定是正常的。这些函数用注释记录了它们的调用约定(输入/输出/修饰符)。

在现实生活中,即使是asm程序(我认为)也倾向于对大多数功能(尤其是在不同的源文件中)使用一致的调用约定,但是任何给定的重要功能都可以执行某些特殊操作。在代码高尔夫球中,您正在优化单个功能中的废话,因此显然这很重要/特别。


要从C程序测试功能,可以编写一个包装器,将args放在正确的位置,保存/恢复您破坏的所有额外寄存器,并将返回值放入(e/rax如果还不存在的话)。


合理的限制:不会给调用者造成不合理负担的任何事情:

  • ESP / RSP必须保留呼叫;其他整数规则是公平的游戏。(RBP和RBX通常按照常规约定保留呼叫,但是您可以同时使用这两种方法。)
  • 任何寄存器(RSP除外)中的任何arg都是合理的,但要求调用者将同一arg复制到多个寄存器中是不合理的。
  • 要求将DF(lods/ stos/等的字符串方向标志)清除(向上)在调用/重拨中是正常的。可以让它在通话/重拨时不确定。要求将其清除或在输入时进行设置,但是在返回时将其保留为修改状态将很奇怪。

  • 在x87中返回FP值st0是合理的,但st3在其他x87寄存器中不带垃圾返回是不合理的。调用者必须清理x87堆栈。即使st0使用非空的较高堆栈寄存器返回也将是有问题的(除非您要返回多个值)。

  • 您的函数将通过调用call,因此[rsp]您的返回地址也是如此。您可以在x86上使用/ 这样的链接寄存器避免call/ ret并使用返回,但这并不是“合理的”。这没有调用/重调用那么有效,因此您可能无法在真实代码中找到它。lea rbx, [ret_addr]jmp functionjmp rbx
  • 在RSP之上破坏无限内存是不合理的,但是在正常的调用约定中,可以在堆栈上破坏函数args。x64 Windows在返回地址上方需要32字节的阴影空间,而x86-64 System V在RSP下为您提供128字节的红色区域,因此这两个都是合理的。(或者甚至更大的红色区域,尤其是在独立程序而不是功能中)。

临界情况:写一个在数组中产生序列的函数,给定前两个元素作为函数args我选择让调用者将序列的开始存储到数组中,然后仅将指针传递给数组。这肯定是在弯曲问题的要求。我考虑将参量打包为xmm0for movlps [rdi], xmm0,这也是一个怪异的调用约定。


在FLAGS中返回布尔值(条件代码)

OS X系统调用执行此操作(CF=0表示没有错误):将标志寄存器用作布尔值返回值是否被认为是不好的做法?

可以使用一个JCC进行检查的任何条件都是完全合理的,尤其是如果您可以选择与该问题有语义相关性的条件。(例如,比较函数可能会设置标志,因此jne如果它们不相等,则将被采用)。


要求将窄args(如a char)作为符号或零,以扩展为32或64位。

这不是不合理的。在现代x86 asm中,使用movzxmovsx 避免部分寄存器变慢是很正常的。实际上,clang / LLVM已经制作了依赖于x86-64 System V调用约定的未公开文档扩展的代码:小于32位的args由调用者符号化或为0扩展为32位

如果需要,您可以通过编写uint64_tint64_t在原型中记录/描述64位扩展。例如,因此您可以使用一条loop指令,该指令使用RCX的全部64位,除非您使用地址大小前缀将大小覆盖为32位ECX(是的,实际上,地址大小不是操作数大小)。

注意,long在Windows 64位ABI和Linux x32 ABI中,这只是32位类型。uint64_t是明确的,比短unsigned long long


现有的呼叫约定:

  • Windows 32位__fastcall已经由另一个答案建议ecxand中的整数args edx

  • x86-64系统V:在寄存器中传递大量的args,并且具有很多调用中断的寄存器,您可以在不使用REX前缀的情况下使用它们。更重要的是,实际上是选择它来允许编译器轻松地内联memcpy或memset rep movsb:前6个整数/指针args在RDI,RSI,RDX,RCX,R8,R9中传递。

    如果您的函数在运行时间的循环内(使用指令)使用lodsd/ ,则可以说“ 与x86-64 System V调用约定一样,可以从C 调用”。 例如:chromakeystosdrcxloopint foo(int *rdi, const int *rsi, int dummy, uint64_t len)

  • 32位GCC regparmEAX,ECX,EDX中的整数args,以EAX(或EDX:EAX)返回。将第一个arg与返回值放在同一寄存器中可以进行一些优化,例如这种情况的示例调用者和带有function属性的原型。当然,AL / EAX对于某些说明也很特殊。

  • Linux x32 ABI在长模式下使用32位指针,因此您可以在修改指针时保存REX前缀(示例用例)。您仍然可以使用64位地址大小,除非您在寄存器中有一个零扩展的32位负整数(因此,如果这样做的话,它将是一个很大的无符号值[rdi + rdx])。

    请注意,push rsp/ pop rax是2个字节,等效于mov rax,rsp,因此您仍然可以2个字节复制完整的64位寄存器。


当挑战要求返回数组时,您认为在堆栈上返回是否合理?我认为这就是编译器在按值返回结构时会做的事情。
qwr

@qwr:不,主流调用约定将隐藏的指针传递给返回值。(某些约定在寄存器中传递/返回小结构)。 C / C ++在幕后按值返回struct,请参见“ 在汇编级对象如何在x86中工作”的结尾。请注意,传递数组(在内部结构中)确实会将它们复制到x86-64 SysV的堆栈中:根据AMD64 ABI数组是哪种C11数据类型,但是Windows x64传递非const指针。
彼得·科德斯

那么您认为合理与否?您是否根据此规则计算x86 codegolf.meta.stackexchange.com/a/8507/17360
qwr

1
@qwr:x86不是“基于堆栈的语言”。x86是带有RAM的寄存器机,而不是堆栈机。堆栈机就像反向抛光符号一样,如x87寄存器。fld / fld / faddp。x86的调用堆栈不适合该模型:所有常规调用约定都使RSP保持不变,或使用ret 16; 弹出args 。他们不会弹出返回地址,先推送一个数组,然后是push rcx/ ret。调用者必须知道数组的大小,或者将RSP保存在堆栈外部的某个地方才能找到自己。
彼得·科德斯

调用将栈中jmp调用后的指令地址压入被调用函数;ret从堆栈中弹出地址并跳转到该地址
RosLuP

7

对AL / AX / EAX使用特殊情况的短格式编码,以及其他短格式和单字节指令

示例假定为32/64位模式,其中默认操作数大小为32位。操作数大小的前缀将指令更改为AX而不是EAX(或在16位模式下为相反)。

  • inc/dec寄存器(8位除外):inc eax/ dec ebp。(不是x86-64:0x4x操作码字节被重新用作REX前缀,所以inc r/m32也是唯一的编码。)

    8位inc bl是2个字节,使用inc r/m8操作码+ ModR / M操作数编码。因此,使用inc ebx以增量bl,如果它是安全的。(例如,如果在高字节可能不为零的情况下不需要ZF结果)。

  • scasde/rdi+=4,要求寄存器指向可读存储器。即使您不在乎FLAGS结果,有时也很有用(例如cmp eax,[rdi]/ rdi+=4)。和在64位模式下,scasb可以作为1字节inc rdi,如果LODSB或STOSB是没有用的。

  • xchg eax, r32:这是0x90 NOP的来源:xchg eax,eax。示例:在8个字节的GCDxchgcdq/ idiv循环中用两个指令为3个寄存器重新排列3个寄存器,其中大多数指令为单字节,包括滥用inc ecx/ loop代替test ecx,ecx/jnz

  • cdq:将EAX符号扩展到EDX:EAX,即将EAX的高位复制到EDX的所有位。用已知的非负数创建零,或用0 / -1进行加/减或掩码。 x86历史课程:cltqvsmovslq。,还有AT&T与Intel助记符有关cdqe

  • lodsb / d:喜欢mov eax, [rsi]/ rsi += 4没有破坏标志。(假设DF是清楚的,则函数输入需要哪些标准调用约定。)也是stosb / d,有时是scas,很少是movs / cmps。

  • push/ pop reg。例如在64位模式下,push rsp/ pop rdi是2个字节,但mov rdi, rsp需要一个REX前缀,并且是3个字节。

xlatb存在,但很少有用。避免使用较大的查找表。我也从未发现可用于AAA / DAA或其他打包的BCD或2 ASCII数字指令。

1字节lahf/ sahf很少有用。您可以 lahf / and ah, 1替代setc ah,但通常没有用。

特别是对于CF,sbb eax,eax将获得0 / -1,甚至是未记录但普遍支持的1字节salc(由Carry设置为AL),在sbb al,al不影响标志的情况下有效地起作用。(在x86-64中删除)。我在用户赞赏挑战#1:Dennis♦中使用了SALC 。

1字节cmc/ clc/ stc(翻转(“补体”),清除或设置CF)是很少有用,虽然我发现了使用cmc在扩展精度除了用碱10 ^ 9块。为了无条件设置/清除CF,通常将其安排为另一条指令的一部分,例如xor eax,eax清除CF和EAX。对于其他条件标志,只有DF(字符串方向)和IF(中断)没有等效的指令。进位标志对于许多指令来说都是特殊的。移位设置它,adc al, 0可以将其添加到2个字节的AL中,我之前提到了未记录的SALC。

std/ cld很少值得。尤其是在32位代码中,最好仅对ALU指令使用dec指针和a mov或内存源操作数,而不用将DF设置为so 向下lodsb/ stosb而不是向上。通常,如果你在所有的向下需要,你还有另一个指针往上走,所以你需要不止一个std,并cld在全功能使用lods/ stos两个。相反,仅使用向上的字符串指令即可。(标准调用约定保证函数输入上的DF = 0,因此您可以免费假设不使用cld。)


8086的历史:为什么存在这些编码

在最初的8086,AX是非常特殊:说明喜欢lodsb/ stosbcbwmul/ div和其他隐含的使用它。当然还是这样。当前的x86尚未删除8086的任何操作码(至少不是任何正式记录的操作码)。但是后来的CPU添加了新的指令,这些指令提供了更好/更有效的方法来执行操作,而无需先将它们复制或交换到AX。(或以32位模式转到EAX。)

例如8086缺少后面的加法,例如movsx/ movzx来加载或移动+符号扩展,或2和3操作数imul cx, bx, 1234,它们不会产生上半部分的结果并且没有任何隐式操作数。

而且,8086的主要瓶颈是指令提取,因此优化代码大小对于当时的性能至关重要。8086 的ISA设计师(Stephen Morse)在AX / AL的特殊情况下花了很多操作码编码空间,包括特殊的(E)AX / AL目的地操作码,用于所有基本的Instant-src ALU-指令,只是opcode + Instant没有ModR / M字节。2字节add/sub/and/or/xor/cmp/test/... AL,imm8AX,imm16或(在32位模式下)EAX,imm32

但是,没有特殊情况EAX,imm8,因此的常规ModR / M编码add eax,4更短。

假设是,如果您要处理某些数据,则需要在AX / AL中进行操作,因此与AX交换寄存器是您可能想要做的事情,甚至可能比寄存器复制到AX时更经常mov

大约8086指令编码的所有内容都支持该范例,从类似指令lodsb/w到EAX立即数的所有特殊情况编码到隐式使用(甚至用于乘法/除法)。


不要被带走;将所有内容交换到EAX并非自动取胜,特别是如果您需要使用32位寄存器而不是8位寄存器的立即数。或者,如果您需要一次交错操作寄存器中的多个变量。或者,如果您使用带有2个寄存器的指令,则根本不使用立即数。

但请始终牢记:我在做EAX / AL短一些的事情吗?我可以重新排列一下,以便在AL中使用它吗?或者我目前在将AL用于它的用途上是否更好地利用了AL。

只要安全就可以自由地混合使用8位和32位运算,以利用它(您不需要将其转入完整寄存器或其他任何东西)。


cdq在许多情况下对于div需要清零edx的情况很有用。
qwr

1
@qwr:是的,如果您知道您的股利低于2 ^ 31(即当视为已签名时为非负数),或者在设置为可能的大数值之前使用它,则可以cdq在未签名之前滥用。通常情况下(外码高尔夫球场)你会使用作为设置为,与diveaxcdqidivxor edx,edxdiv
彼得·柯德斯

5

使用fastcall约定

x86平台有许多调用约定。您应该使用那些在寄存器中传递参数的参数。在x86_64上,无论如何前几个参数都会传递到寄存器中,所以在那里没有问题。在32位平台上,默认的调用约定(cdecl)在堆栈中传递参数,这对打高尔夫球不利-在堆栈上访问参数需要很长的指令。

fastcall在32位平台上使用时,通常在ecx和中传递2个第一个参数edx。如果函数具有3个参数,则可以考虑在64位平台上实现它。

fastcall约定的C函数原型(从此示例答案中获取):

extern int __fastcall SwapParity(int value);                 // MSVC
extern int __attribute__((fastcall)) SwapParity(int value);  // GNU   

或使用完全自定义的调用约定,因为您使用纯asm编写代码,而不必编写要从C调用的代码。在FLAGS中返回布尔值通常很方便。
彼得·科德斯


4

用3创建零mul(然后inc/ dec以获得+1 / -1以及零)

您可以通过在第三个寄存器中乘以零来使eax和edx归零。

xor   ebx, ebx      ; 2B  ebx = 0
mul   ebx           ; 2B  eax=edx = 0

inc   ebx           ; 1B  ebx=1

将导致EAX,EDX和EBX在四个字节中都为零。您可以将EAX和EDX归零为三个字节:

xor eax, eax
cdq

但是从那开始,您将无法再在一个字节中获得第三个零寄存器,或者在另外2个字节中获得+1或-1寄存器。而是使用mul技术。

用例示例:将Fibonacci数字连接为binary

请注意,LOOP循环结束后,ECX将为零,并可用于将EDX和EAX归零。您不必总是使用创建第一个零xor


1
这有点令人困惑。你能扩大吗?
NoOneIsHere17年

@NoOneIsHere我相信他想将三个寄存器设置为0,包括EAX和EDX。
NieDzejkob

4

CPU寄存器和标志处于已知的启动状态

我们可以假定CPU处于基于平台和OS的已知且已记录的默认状态。

例如:

DOS http://www.fysnet.net/yourhelp.htm

Linux x86 ELF http://asm.sourceforge.net/articles/startup.html


1
代码高尔夫规则说,您的代码必须在至少一种实现上起作用。即使i386和x86-64 System V ABI文档说它们在进入时是“未定义的”,Linux还是选择将所有reg(RSP除外)和堆栈归零,然后再进入新的用户空间进程_start。因此,是的,如果您要编写程序而不是函数,可以利用它。我是在Extreme Fibonacci中这样做的。(在动态链接的可执行文件中,ld.so在跳转到您的之前运行_start,并且确实在寄存器留下了垃圾,但是静态只是您的代码。)
Peter Cordes

3

要加减1,请使用一个字节incdec比多字节加法和减法子指令小的指令。


请注意,32位模式只有1个字节inc/dec r32,其寄存器号已在操作码中编码。所以inc ebx是1个字节,但是inc bl是2个。仍然小于add bl, 1当然对于比其他寄存器al。另请注意,inc/ dec保留CF不变,但更新其他标志。
彼得·科德斯

1
2 x86中的+2和-2
l4m2,18年

3

lea 数学

这可能是了解x86的第一件事,但在此提醒我。lea可用于乘以2、3、4、5、8或9,然后加上一个偏移量。

例如,要ebx = 9*eax + 3在一条指令中进行计算(在32位模式下):

8d 5c c0 03             lea    0x3(%eax,%eax,8),%ebx

这里没有偏移量:

8d 1c c0                lea    (%eax,%eax,8),%ebx

哇!当然,lea也可以用来做数学运算,例如ebx = edx + 8*eax + 3计算数组索引。


1
也许值得一提的lea eax, [rcx + 13]是64位模式的无前缀版本。32位操作数大小(用于结果)和64位地址大小(用于输入)。
彼得·科德斯

3

循环和字符串指令比替代指令序列小。最有用的是loop <label>它小于两个指令序列dec ECXand jnz <label>,并且lodsb小于mov al,[esi]and inc si


2

mov 适用时将小立即数放入较低的寄存器

如果您已经知道寄存器的高位为0,则可以使用较短的指令将立即数移入低位寄存器。

b8 0a 00 00 00          mov    $0xa,%eax

b0 0a                   mov    $0xa,%al

使用push/pop表示imm8高零位

归功于Peter Cordes。xor/ mov是4个字节,但是push/ pop只有3 个字节!

6a 0a                   push   $0xa
58                      pop    %eax

mov al, 0xa如果您不需要将其零扩展到完整的reg,那就很好。但是,如果这样做,则xor / mov为4字节,而推送imm8 / pop或lea其他已知常量为3字节。如果mul零个3个4个字节的寄存器或组合使用这可能很有cdq,但是如果您需要很多常量。
彼得·科德斯

另一个用例是来自的常量[0x80..0xFF],这些常量不能表示为符号扩展的imm8。或者,如果您已经知道高字节,例如mov cl, 0x10在执行一条loop指令之后,因为loop不跳转的唯一方法是在执行时rcx=0。(我想您是这样说的,但是您的示例使用了xor)。您甚至可以将寄存器的低字节用于其他内容,只要完成后其他内容会将其恢复为零(或其他任何值)即可。例如,我的斐波那契程序保留-1024在ebx中,并使用bl。
彼得·科德斯

@PeterCordes我已经添加了您的推入/弹出技术
qwr

应该考虑有关常量的现有答案,而anatolyg已经在注释中提出了建议。我将编辑该答案。IMO应返工这一个使用8位操作数大小为更多的东西(除了建议xchg eax, r32),例如mov bl, 10/ dec bl/ jnz所以你的代码不关心高字节RBX的。
彼得·科德斯

@PeterCordes嗯。我仍然不确定何时使用8位操作数,因此我不确定要在该答案中添加什么。
qwr

2

标志位是许多指令后设置

经过许多算术指令后,进位标志(无符号)和溢出标志(有符号)会自动设置(更多信息)。经过许多算术和逻辑运算后,才设置符号标志和零标志。这可以用于条件分支。

例:

d1 f8                   sar    %eax

ZF由该指令设置,因此我们可以将其用于常规分支。


您何时使用过奇偶校验标志?您知道这是结果的低8位的水平异或,对吗?(不论操作数大小的,PF仅从低8位设定 ;另见)。不是偶数/奇数;之后的ZF支票test al,1; 您通常不会免费获得。(或and al,1根据奇/偶创建一个整数0/1。)
Peter Cordes

无论如何,如果这个回答说“使用已经由其他指令设置的标志来避免test/ cmp”,那么那将是非常基本的初学者x86,但仍然值得赞扬。
彼得·科德斯

@PeterCordes Huh,我似乎误解了平价标志。我仍在努力寻找其他答案。我将编辑答案。您可能会说,我是一个初学者,因此基本提示会有所帮助。
qwr

2

使用do-while循环代替while循环

这不是x86专用的,而是广泛适用的初学者组装技巧。如果您知道while循环将至少运行一次,请将循环重写为do-while循环,并在最后进行循环条件检查,通常会保存2字节的跳转指令。在特殊情况下,您甚至可以使用loop


2
相关:为什么循环总是这样编译?解释了为什么do{}while()组装中自然循环成语(特别是为了提高效率)。还要注意,2字节jecxz/ jrcxz循环前可以很好loop地处理“需要零次运行”的情况(在loop不慢的罕见CPU上)“有效地”处理。 jecxz可使用内部循环来实现while(ecx){},以jmp在底部。
彼得·科德斯

@PeterCordes是一个非常好的书面答案。我想在代码高尔夫程序中找到跳入循环中间的用途。
qwr

使用goto jmp和缩进...循环跟随
RosLuP

2

使用任何方便的调用约定

System V的x86使用堆栈和System V X86-64用途rdirsirdxrcx,等输入参数,并rax作为返回值,但它是完全合理的使用自己的调用约定。__fastcall使用ecxedx作为输入参数,其他编译器/ OS使用它们自己的约定。方便时,使用堆栈和任何寄存器作为输入/输出。

示例:重复字节计数器,对1字节解决方案使用了巧妙的调用约定。

Meta:将输入写入寄存器将输出写入寄存器

其他资源:Agner Fog关于调用约定的注释


1
我终于开始对这个关于制定调用约定的问题发表自己的答案,这是合理的还是不合理的。
彼得·科德斯

@PeterCordes不相关,在x86中打印的最佳方法是什么?到目前为止,我避免了需要打印的挑战。DOS看起来对I / O具有有用的中断,但是我只打算编写32/64位答案。我知道的唯一方法是int 0x80需要大量设置。
qwr

是的,int 0x80用32位或syscall64位代码来调用sys_write是唯一的好方法。这就是我用于Extreme Fibonacci的方式。在64位代码中,__NR_write = 1 = STDOUT_FILENO您可以这样做mov eax, edi。或者,如果EAX的高位字节为零,则为mov al, 432位代码。您也可以,call printf或者puts,我想写一个“用于Linux + glibc的x86 asm”答案。我认为不计算PLT或GOT入口空间或库代码本身是合理的。
彼得·科德斯

1
我更倾向于让调用者传递a char*buf并以手动格式生成字符串。例如像这样(笨拙地优化了速度)asm FizzBu​​zz,我在其中将字符串数据存入寄存器,然后用进行存储mov,因为字符串较短且长度固定。
彼得·科德斯

1

使用条件移动CMOVcc和集合SETcc

这更让我想起了问题,但是在处理器P6(Pentium Pro)或更高版本上存在条件设置指令,并且存在条件移动指令。有许多指令基于EFLAGS中设置的一个或多个标志。


1
我发现分支通常较小。在某些情况下,它是自然适合的,但是cmov具有2字节的操作码(0F 4x +ModR/M),因此最少3字节。但是源是r / m32,因此您可以有条件地加载3个字节。除了分支以外,setcc在更多情况下还有用cmovcc。尽管如此,请考虑整个指令集,而不仅仅是基线386指令。(尽管SSE2和BMI / BMI2指令是如此之大,以至于它们很少有用。它 rorx eax, ecx, 32是6字节,比mov + ror长。对于性能而言,这是不错的选择,除非POPCNT或PDEP节省了许多isns,否则对高尔夫来说不是很好)
Peter Cordes

@PeterCordes谢谢,我补充说setcc
qwr

1

jmp通过安排if / then而不是if / then / else 节省字节

这当然是非常基本的,只是想我会把它发布为打高尔夫球时要考虑的事情。例如,请考虑以下简单代码来解码十六进制数字字符:

    cmp $'A', %al
    jae .Lletter
    sub $'0', %al
    jmp .Lprocess
.Lletter:
    sub $('A'-10), %al
.Lprocess:
    movzbl %al, %eax
    ...

通过使“ then”情况变为“ else”情况,可以将其缩短两个字节:

    cmp $'A', %al
    jb .digit
    sub $('A'-'0'-10), %eax
.digit:
    sub $'0', %eax
    movzbl %al, %eax
    ...

在优化性能时,通常会通常这样做,尤其是在sub一种情况下,关键路径上的额外延迟不是循环携带的依赖链的一部分时(例如,此处每个输入数字在合并4位块之前都是独立的) )。但是我还是想+1。顺便说一句,您的示例有一个单独的错过的优化:如果仍然需要movzx在末尾使用a ,则sub $imm, %al不要使用EAX来利用的无现代2字节编码op $imm, %al
Peter Cordes

另外,您可以cmp通过执行操作消除sub $'A'-10, %aljae .was_alpha; add $('A'-10)-'0'。(我认为我的逻辑正确)。请注意,'A'-10 > '9'因此没有歧义。减去字母的更正将包装一个十进制数字。因此,如果像您一样假定输入是有效的十六进制,这是安全的。
Peter Cordes

0

您可以通过将esi设置为esp并执行一系列lodsd / xchg reg eax来从堆栈中获取顺序对象。


这是为什么优于pop eax/ pop edx/ ...?如果需要将它们留在堆栈上,则可以push在恢复ESP之后将它们全部恢复,每个对象仍然需要2个字节,而无需mov esi,esp。还是对64位代码中的4字节对象意味着pop8字节?顺便说一句,您甚至可以使用pop比性能更好的缓冲区循环lodsd,例如在Extreme Fibonacci中扩展精度添加
Peter Cordes

它在“ lea esi,[esp + ret地址的大小]”之后更正确地有用,除非您有备用寄存器,否则它将排除使用pop。
彼得·费里

哦,关于函数args?非常罕见的是,您想要的args超过了寄存器的数量,或者您希望调用者将一个args保留在内存中,而不是将它们全部传递给寄存器。(如果标准注册调用约定之一不完全适合,我对使用自定义调用约定的回答有一半完成了)
Peter Cordes

使用cdecl而不是fastcall会将参数保留在堆栈中,并且很容易拥有很多参数。例如,请参见github.com/peterferrie/tinycrypt。
彼得·费里

0

对于代码高尔夫和ASM:使用说明仅使用寄存器,下推弹出,最大程度地减少寄存器内存或立即存储


0

要复制64位寄存器,请使用push rcxpop rdx而不是3个字节mov
push / pop的默认操作数大小为64位,而无需REX前缀。

  51                      push   rcx
  5a                      pop    rdx
                vs.
  48 89 ca                mov    rdx,rcx

(操作数大小的前缀可以将push / pop操作数大小改写为16位,但是即使在REX.W = 0的情况下,在64位模式下无法编码32位push / pop操作数大小。)

如果一个或两个寄存器都是r8.. r15,请使用,mov因为push和/或pop将需要REX前缀。最糟糕的情况是,如果两个都需要REX前缀,则实际上会丢失。显然,在代码高尔夫中,您通常应该避免使用r8..r15。


使用此NASM宏进行开发时,可以使源代码更具可读性。只需记住,它在RSP下面的8个字节上移动。(在x86-64 System V中的红色区域中)。但在正常情况下,它可以直接替代64位mov r64,r64mov r64, -128..127

    ; mov  %1, %2       ; use this macro to copy 64-bit registers in 2 bytes (no REX prefix)
%macro MOVE 2
    push  %2
    pop   %1
%endmacro

例子:

   MOVE  rax, rsi            ; 2 bytes  (push + pop)
   MOVE  rbp, rdx            ; 2 bytes  (push + pop)
   mov   ecx, edi            ; 2 bytes.  32-bit operand size doesn't need REX prefixes

   MOVE  r8, r10             ; 4 bytes, don't use
   mov   r8, r10             ; 3 bytes, REX prefix has W=1 and the bits for reg and r/m being high

   xchg  eax, edi            ; 1 byte  (special xchg-with-accumulator opcodes)
   xchg  rax, rdi            ; 2 bytes (REX.W + that)

   xchg  ecx, edx            ; 2 bytes (normal xchg + modrm)
   xchg  rcx, rdx            ; 3 bytes (normal REX + xchg + modrm)

xchg示例的一部分是因为有时您需要获取EAX或RAX的值,而不必关心保留旧副本。但是,push / pop并不能帮助您进行实际交流。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.