x86 32位机器代码功能,42 41字节
当前最短的非高尔夫语言答案,比1B短 @streetster的q / kdb +。
对于真实,使用0表示,对于虚假使用非零值: 41 40字节。 (通常,对于32位,保存1字节,对于64位,保存2字节)。
使用隐式长度的字符串(C样式以0终止): 45 44字节
x86-64机器代码(带有32位指针,例如x32 ABI): 44 43个字节。
x86-64具有隐式长度的字符串,仍然是46个字节(移位/掩码位图策略现在已达到收支平衡)。
这是带有C签名的函数 _Bool dennis_like(size_t ecx, const char *esi)
。调用约定略微不标准,接近MS向量调用/快速调用,但具有不同的arg寄存器:ESI中的字符串和ECX中的长度。它只会破坏其arg-regs和EDX。AL保留返回值,高字节保留垃圾(如SysV x86和x32 ABI所允许。IDK MS的ABI在返回布尔值或窄整数时所说的关于高垃圾的内容。)
算法说明:
循环输入字符串,过滤并分类为堆栈上的布尔数组:对于每个字节,请检查其是否为字母字符(如果不是,则继续下一个字符),并将其转换为0-25(AZ)的整数。使用该0-25整数来检查元音= 0 /辅音= 1的位图。(位图作为32位立即数加载到寄存器中)。根据位图结果将0或0xFF压入堆栈(实际上是在32位元素的低字节中,前3个字节中可能有垃圾)。
第一个循环产生一个0或0xFF的数组(在用垃圾填充的dword元素中)。使用第二个循环进行通常的回文检查,该循环在指针在中间交叉时停止(或者,如果字母字符的奇数都指向相同的元素,则它们都指向同一元素)。向上移动的指针是堆栈指针,我们使用POP进行加载和递增。代替此循环中的compare / setcc,我们可以仅使用XOR来检测相同/不同,因为只有两个可能的值。我们可以累加(使用OR)是否找到任何不匹配的元素,但是在XOR设置的标志上的提前分支至少是一样好。
请注意,第二个循环使用byte
操作数大小,因此它不在乎第一个循环在每个数组元素的低字节之外留下了什么垃圾。
它使用未记录的salc
指令以相同的方式从CF设置AL sbb al,al
。每个Intel CPU(64位模式除外)都支持它,甚至Knight's Landing! Agner Fog列出了它的时间安排了所有AMD CPU(包括Ryzen)上的,因此,如果x86供应商自8086起一直坚持占用该字节的操作码空间,我们也可能会利用它。
有趣的技巧:
- 无符号比较技巧结合使用isalpha()和toupper()的,并将该字节零扩展以填充eax,设置为:
- 的寄存器中的立即位图
bt
,受一些不错的编译器输出的启发switch
。
- 通过推入循环在堆栈上创建一个可变大小的数组。(asm的标准,但对于隐式长度字符串版本,您不能使用C来做)。它为每个输入字符使用4字节的堆栈空间,但与最佳打球相比至少节省了1字节
stosb
。
- 代替布尔数组上的cmp / setne,XOR布尔在一起直接获得真值。(
cmp
/ salc
不是一个选项,因为salc
仅适用于CF,而0xFF-0并未设置CF. sete
为3字节,但避免inc
了循环外,净成本为2字节(在64位模式下为1 ))与循环中的xor并使用inc。
; explicit-length version: input string in ESI, byte count in ECX
08048060 <dennis_like>:
8048060: 55 push ebp
8048061: 89 e5 mov ebp,esp ; a stack frame lets us restore esp with LEAVE (1B)
8048063: ba ee be ef 03 mov edx,0x3efbeee ; consonant bitmap
08048068 <dennis_like.filter_loop>:
8048068: ac lods al,BYTE PTR ds:[esi]
8048069: 24 5f and al,0x5f ; uppercase
804806b: 2c 41 sub al,0x41 ; range-shift to 0..25
804806d: 3c 19 cmp al,0x19 ; reject non-letters
804806f: 77 05 ja 8048076 <dennis_like.non_alpha>
8048071: 0f a3 c2 bt edx,eax # AL = 0..25 = position in alphabet
8048074: d6 SALC ; set AL=0 or 0xFF from carry. Undocumented insn, but widely supported
8048075: 50 push eax
08048076 <dennis_like.non_alpha>:
8048076: e2 f0 loop 8048068 <dennis_like.filter_loop> # ecx = remaining string bytes
; end of first loop
8048078: 89 ee mov esi,ebp ; ebp = one-past-the-top of the bool array
0804807a <dennis_like.palindrome_loop>:
804807a: 58 pop eax ; read from the bottom
804807b: 83 ee 04 sub esi,0x4
804807e: 32 06 xor al,BYTE PTR [esi]
8048080: 75 04 jne 8048086 <dennis_like.non_palindrome>
8048082: 39 e6 cmp esi,esp ; until the pointers meet or cross in the middle
8048084: 77 f4 ja 804807a <dennis_like.palindrome_loop>
08048086 <dennis_like.non_palindrome>:
; jump or fall-through to here with al holding an inverted boolean
8048086: 40 inc eax
8048087: c9 leave
8048088: c3 ret
;; 0x89 - 0x60 = 41 bytes
这可能也是最快的答案之一,因为打高尔夫球的痛感并没有太严重,至少对于少于几千个字符的字符串而言,这种情况下4倍的内存使用不会造成很多缓存丢失。(它可能还会丢失在循环所有字符之前需要非丹尼斯类字符串的提早回答的答案。) salc
比setcc
许多CPU上的速度慢(例如3 uops与Skylake上的1),但是使用了位图检查bt/salc
仍然比字符串搜索或正则表达式匹配快。而且没有启动开销,因此对于短字符串而言非常便宜。
快速进行一次操作将意味着重复上下方向的分类代码。那会更快,但是代码更大。(当然,如果需要快速,您可以使用SSE2或AVX2一次处理16个或32个字符,但仍然可以通过将范围移动到有符号范围的底部来使用比较技巧)。
测试程序(对于ia32或x32 Linux),以cmdline arg调用此函数,并以status =返回值退出。 strlen
从int80h.org实现。
; build with the same %define macros as the source below (so this uses 32-bit regs in 32-bit mode)
global _start
_start:
;%define PTRSIZE 4 ; true for x32 and 32-bit mode.
mov esi, [rsp+4 + 4*1] ; esi = argv[1]
;mov rsi, [rsp+8 + 8*1] ; rsi = argv[1] ; For regular x86-64 (not x32)
%if IMPLICIT_LENGTH == 0
; strlen(esi)
mov rdi, rsi
mov rcx, -1
xor eax, eax
repne scasb ; rcx = -strlen - 2
not rcx
dec rcx
%endif
mov eax, 0xFFFFAEBB ; make sure the function works with garbage in EAX
call dennis_like
;; use the 32-bit ABI _exit syscall, even in x32 code for simplicity
mov ebx, eax
mov eax, 1
int 0x80 ; _exit( dennis_like(argv[1]) )
;; movzx edi, al ; actually mov edi,eax is fine here, too
;; mov eax,231 ; 64-bit ABI exit_group( same thing )
;; syscall
可以使用此函数的64位版本,该版本sbb eax,eax
只有2个字节,而不是3个字节setc al
。它还需要一个额外的字节,dec
或not
在结束(因为只有32位有1个字节INC / DEC R32)。使用x32 ABI(长模式下的32位指针),即使我们复制和比较指针,我们仍然可以避免使用REX前缀。
setc [rdi]
可以直接写到内存,但是保留ECX字节的堆栈空间比节省更多的代码大小。(而且我们需要遍历输出数组。 [rdi+rcx]
在寻址模式下要多花一个字节,但实际上我们需要一个不会针对过滤后的字符进行更新的计数器,因此它会变得更糟。)
这是带有%if
条件的YASM / NASM源。可以使用-felf32
(32位代码)或-felfx32
(使用x32 ABI使用64位代码)以及隐式或显式length 构建。我已经测试了所有4个版本。请参阅此答案以获取用于从NASM / YASM源构建静态二进制文件的脚本。
要在不支持x32 ABI的计算机上测试64位版本,可以将指针regs更改为64位。(然后简单地从计数中减去REX.W = 1前缀的数量(0x48字节)。在这种情况下,需要4条指令使用REX前缀才能对64位寄存器进行操作)。或者,只需rsp
在地址空间低4G中使用和输入指针来调用它。
%define IMPLICIT_LENGTH 0
; This source can be built as x32, or as plain old 32-bit mode
; x32 needs to push 64-bit regs, and using them in addressing modes avoids address-size prefixes
; 32-bit code needs to use the 32-bit names everywhere
;%if __BITS__ != 32 ; NASM-only
%ifidn __OUTPUT_FORMAT__, elfx32
%define CPUMODE 64
%define STACKWIDTH 8 ; push / pop 8 bytes
%else
%define CPUMODE 32
%define STACKWIDTH 4 ; push / pop 4 bytes
%define rax eax
%define rcx ecx
%define rsi esi
%define rdi edi
%define rbp ebp
%define rsp esp
%endif
; A regular x86-64 version needs 4 REX prefixes to handle 64-bit pointers
; I haven't cluttered the source with that, but I guess stuff like %define ebp rbp would do the trick.
;; Calling convention similar to SysV x32, or to MS vectorcall, but with different arg regs
;; _Bool dennis_like_implicit(const char *esi)
;; _Bool dennis_like_explicit(size_t ecx, const char *esi)
global dennis_like
dennis_like:
; We want to restore esp later, so make a stack frame for LEAVE
push rbp
mov ebp, esp ; enter 0,0 is 4 bytes. Only saves bytes if we had a fixed-size allocation to do.
; ZYXWVUTSRQPONMLKJIHGFEDCBA
mov edx, 11111011111011111011101110b ; consonant/vowel bitmap for use with bt
;;; assume that len >= 1
%if IMPLICIT_LENGTH
lodsb ; pipelining the loop is 1B shorter than jmp .non_alpha
.filter_loop:
%else
.filter_loop:
lodsb
%endif
and al, 0x7F ^ 0x20 ; force ASCII to uppercase.
sub al, 'A' ; range-shift to 'A' = 0
cmp al, 'Z'-'A' ; if al was less than 'A', it will be a large unsigned number
ja .non_alpha
;; AL = position in alphabet (0-25)
bt edx, eax ; 3B
%if CPUMODE == 32
salc ; 1B only sets AL = 0 or 0xFF. Not available in 64-bit mode
%else
sbb eax, eax ; 2B eax = 0 or -1, according to CF.
%endif
push rax
.non_alpha:
%if IMPLICIT_LENGTH
lodsb
test al,al
jnz .filter_loop
%else
loop .filter_loop
%endif
; al = potentially garbage if the last char was non-alpha
; esp = bottom of bool array
mov esi, ebp ; ebp = one-past-the-top of the bool array
.palindrome_loop:
pop rax
sub esi, STACKWIDTH
xor al, [rsi] ; al = (arr[up] != arr[--down]). 8-bit operand-size so flags are set from the non-garbage
jnz .non_palindrome
cmp esi, esp
ja .palindrome_loop
.non_palindrome: ; we jump here with al=1 if we found a difference, or drop out of the loop with al=0 for no diff
inc eax ;; AL transforms 0 -> 1 or 0xFF -> 0.
leave
ret ; return value in AL. high bytes of EAX are allowed to contain garbage.
我看着弄乱DF(控制lodsd
/ 的方向标志,scasd
依此类推),但似乎并没有取得成功。通常的ABI要求在函数进入和退出时清除DF。IMO认为,如果在进入时已清除但在退出时将其设置为欺骗。最好使用LODSD / SCASD来避免使用3个字节sub esi, 4
,尤其是在没有高容量垃圾的情况下。
备用位图策略(用于x86-64隐式长度的字符串)
事实证明,这不会节省任何字节,因为bt r32,r32
在位索引中仍然可以处理大量垃圾。只是没有记录方式shr
。
而不是bt / sbb
让位进入CF或从CF中移出,请使用shift /掩码将所需的位与位图隔离。
%if IMPLICIT_LENGTH && CPUMODE == 64
; incompatible with LOOP for explicit-length, both need ECX. In that case, bt/sbb is best
xchg eax, ecx
mov eax, 11111011111011111011101110b ; not hoisted out of the loop
shr eax, cl
and al, 1
%else
bt edx, eax
sbb eax, eax
%endif
push rax
由于这将在AL末尾产生0/1(而不是0 / 0xFF),因此我们可以在函数末尾使用xor al, 1
(2B)而不是dec eax
(x86-64中也为2B)对返回值进行必要的反转仍然会产生适当的bool
/_Bool
返回值。
通过避免将EAX的高字节清零,这通常为x86-64使用隐式长度的字符串节省了1B。(我一直在用and eax, 0x7F ^ 0x20
3字节强制将其余的eax大写和置零and r32,imm8
。但是现在我正在使用大多数8086指令具有的2字节即时AL编码,就像我已经在做的那样为sub
和cmp
。)
在32位模式下,它会丢失bt
/ salc
,并且显式长度的字符串需要ECX进行计数,因此在此也不起作用。
但是后来我意识到我错了:bt edx, eax
在eax中仍然可以处理大量垃圾。它显然掩盖了移位数以同样的方式shr r32, cl
也(在CL的低5位仅看)。这与不同bt [mem], reg
,后者可以在寻址模式/大小所引用的内存外部访问,将其视为位串。(疯狂的CISC ...)
英特尔的insn set ref手册没有记录屏蔽操作,因此,英特尔目前可能保留的是未记录的行为。(这种情况并不少见 bsf dst, src
。src = 0的情况下,即使dst始终保持未定义的值,但dst始终保持不变。AMD实际上记录了src = 0的行为。)我在Skylake和Core2上进行了测试,该bt
版本适用于AL以外的EAX中的非零垃圾。
一个巧妙的技巧是使用xchg eax,ecx
(1个字节)将计数计入CL。不幸的是,BMI2 shrx eax, edx, eax
是5个字节,而仅为2个字节shr eax, cl
。using bextr
需要一个2字节mov ah,1
(用于提取的位数),因此它又是5 + 2字节,例如SHRX + AND。
添加%if
条件后,源代码变得非常混乱。 这是x32隐式长度字符串的分解(使用位图的替代策略,因此仍然是46个字节)。
与显式长度版本的主要区别在于第一个循环。注意lods
在它的前面和底部有一个,而不是在循环的顶部只有一个。
; 64-bit implicit-length version using the alternate bitmap strategy
00400060 <dennis_like>:
400060: 55 push rbp
400061: 89 e5 mov ebp,esp
400063: ac lods al,BYTE PTR ds:[rsi]
00400064 <dennis_like.filter_loop>:
400064: 24 5f and al,0x5f
400066: 2c 41 sub al,0x41
400068: 3c 19 cmp al,0x19
40006a: 77 0b ja 400077 <dennis_like.non_alpha>
40006c: 91 xchg ecx,eax
40006d: b8 ee be ef 03 mov eax,0x3efbeee ; inside the loop since SHR destroys it
400072: d3 e8 shr eax,cl
400074: 24 01 and al,0x1
400076: 50 push rax
00400077 <dennis_like.non_alpha>:
400077: ac lods al,BYTE PTR ds:[rsi]
400078: 84 c0 test al,al
40007a: 75 e8 jne 400064 <dennis_like.filter_loop>
40007c: 89 ee mov esi,ebp
0040007e <dennis_like.palindrome_loop>:
40007e: 58 pop rax
40007f: 83 ee 08 sub esi,0x8
400082: 32 06 xor al,BYTE PTR [rsi]
400084: 75 04 jne 40008a <dennis_like.non_palindrome>
400086: 39 e6 cmp esi,esp
400088: 77 f4 ja 40007e <dennis_like.palindrome_loop>
0040008a <dennis_like.non_palindrome>:
40008a: ff c8 dec eax ; invert the 0 / non-zero status of AL. xor al,1 works too, and produces a proper bool.
40008c: c9 leave
40008d: c3 ret
0x8e - 0x60 = 0x2e = 46 bytes