Hàm mã máy x86 32 bit, 42 41 byte
Hiện tại, câu trả lời không phải là ngôn ngữ chơi gôn ngắn nhất, ngắn hơn 1B so với q / kdb + của @ streetster .
Với 0 cho trung thực và khác không cho giả mạo: 41 40 byte. (nói chung, tiết kiệm 1 byte cho 32 bit, 2 byte cho 64 bit).
Với các chuỗi có độ dài ngầm định (kiểu C kết thúc 0): 45 44 byte
mã máy x86-64 (với các con trỏ 32 bit, như x32 ABI): 44 43 byte .
x86-64 với các chuỗi có độ dài ẩn, vẫn là 46 byte (chiến lược bitmap shift / mask là hòa vốn ngay bây giờ).
Đây là một chức năng với C chữ ký _Bool dennis_like(size_t ecx, const char *esi)
. Quy ước gọi hơi không chuẩn, gần với MS vectorcall / fastcall nhưng với các thanh ghi arg khác nhau: chuỗi trong ESI và độ dài trong ECX. Nó chỉ chặn các arg-reg và EDX của nó. AL giữ giá trị trả về, với các byte chứa rác cao (như được cho phép bởi SysV x86 và x32 ABI. IDK những gì ABI của MS nói về rác cao khi trả về bool hoặc số nguyên hẹp.)
Giải thích về thuật toán :
Lặp lại chuỗi đầu vào, lọc và phân loại thành một mảng boolean trên ngăn xếp: Đối với mỗi byte, hãy kiểm tra xem đó có phải là ký tự chữ cái không (nếu không, tiếp tục sang char tiếp theo) và chuyển đổi nó thành một số nguyên từ 0-25 (AZ) . Sử dụng số nguyên 0-25 đó để kiểm tra một bitmap nguyên âm = 0 / phụ âm = 1. (Bitmap được tải vào một thanh ghi dưới dạng hằng số tức thời 32 bit). Đẩy 0 hoặc 0xFF lên ngăn xếp theo kết quả bitmap (thực tế là trong byte thấp của phần tử 32 bit, có thể có rác trong 3 byte trên cùng).
Vòng lặp đầu tiên tạo ra một mảng 0 hoặc 0xFF (trong các phần tử từ được đệm bằng rác). Thực hiện kiểm tra palindrom thông thường với một vòng lặp thứ hai dừng lại khi các con trỏ giao nhau ở giữa (hoặc khi cả hai đều trỏ đến cùng một phần tử nếu có một số ký tự chữ cái lẻ). Con trỏ di chuyển lên trên là con trỏ ngăn xếp và chúng tôi sử dụng POP để tải + tăng. Thay vì so sánh / setcc trong vòng lặp này, chúng ta chỉ có thể sử dụng XOR để phát hiện giống / khác vì chỉ có hai giá trị có thể. Chúng tôi có thể tích lũy (với OR) cho dù chúng tôi có tìm thấy bất kỳ yếu tố không phù hợp nào hay không, nhưng một nhánh xuất hiện sớm trên các cờ được đặt bởi XOR ít nhất là tốt.
Lưu ý rằng vòng lặp thứ hai sử dụng byte
kích thước toán hạng, vì vậy nó không quan tâm rác nào mà vòng lặp thứ nhất để lại bên ngoài byte thấp của mỗi phần tử mảng.
Nó sử dụng hướng dẫn không có giấy tờsalc
để đặt AL từ CF, theo cách tương tự sbb al,al
. Nó được hỗ trợ trên mọi CPU Intel (ngoại trừ ở chế độ 64 bit), thậm chí cả Knight's Landing! Agner Fog cũng liệt kê thời gian cho nó trên tất cả các CPU AMD (bao gồm cả Ryzen), vì vậy nếu các nhà cung cấp x86 khăng khăng buộc byte không gian opcode đó kể từ 8086, chúng tôi cũng có thể tận dụng lợi thế của nó.
Thủ thuật thú vị:
- unsign - so sánh thủ thuật cho một isalpha () và toupper () kết hợp và zero - mở rộng byte để điền vào eax, thiết lập cho:
- bitmap ngay lập tức trong một đăng ký cho
bt
, lấy cảm hứng từ một số đầu ra trình biên dịch đẹp choswitch
.
- Tạo một mảng có kích thước thay đổi trên ngăn xếp với một vòng lặp. (Tiêu chuẩn cho mã asm, nhưng không phải là thứ bạn có thể làm với C cho phiên bản chuỗi có độ dài ẩn). Nó sử dụng 4 byte không gian ngăn xếp cho mỗi ký tự đầu vào, nhưng tiết kiệm ít nhất 1 byte so với chơi golf tối ưu xung quanh
stosb
.
- Thay vì cmp / setne trên mảng boolean, các booleans XOR kết hợp với nhau để lấy giá trị thật trực tiếp. (
cmp
/ salc
không phải là một tùy chọn, vì salc
chỉ hoạt động với CF và 0xFF-0 không đặt CF. sete
là 3 byte, nhưng sẽ tránh inc
vòng lặp bên ngoài, với chi phí ròng là 2 byte (chế độ 1 trong 64 bit )) so với xor trong vòng lặp và sửa nó với 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
Đây có lẽ cũng là một trong những câu trả lời nhanh nhất, vì không có môn đánh gôn nào thực sự gây tổn thương quá nặng, ít nhất là cho các chuỗi dưới vài nghìn ký tự trong đó việc sử dụng bộ nhớ 4x không gây ra nhiều lỗi nhớ cache. (Nó cũng có thể thua các câu trả lời sớm cho các chuỗi không giống Dennis trước khi lặp qua tất cả các ký tự.) salc
Chậm hơn so với setcc
nhiều CPU (ví dụ 3 uops so với 1 trên Skylake), nhưng kiểm tra bitmap bằng bt/salc
vẫn nhanh hơn tìm kiếm chuỗi hoặc kết hợp regex. Và không có chi phí khởi động, vì vậy nó cực kỳ rẻ cho các chuỗi ngắn.
Làm điều đó trong một lần khi đang bay có nghĩa là lặp lại mã phân loại cho các hướng lên và xuống. Đó sẽ là nhanh hơn nhưng kích thước mã lớn hơn. (Tất nhiên nếu bạn muốn nhanh, bạn có thể thực hiện 16 hoặc 32 ký tự cùng một lúc với SSE2 hoặc AVX2, vẫn sử dụng thủ thuật so sánh bằng cách dịch chuyển phạm vi xuống dưới cùng của phạm vi đã ký).
Kiểm tra chương trình (đối với ia32 hoặc x32 Linux) để gọi hàm này bằng cmdline arg và thoát với status = giá trị trả về. strlen
triển khai từ 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
Phiên bản 64 bit của chức năng này có thể sử dụng sbb eax,eax
, chỉ có 2 byte thay vì 3 cho setc al
. Nó cũng sẽ cần thêm một byte cho dec
hoặc not
ở cuối (vì chỉ 32 bit có inc / dec r32 1 byte). Sử dụng x32 ABI (con trỏ 32 bit ở chế độ dài), chúng tôi vẫn có thể tránh các tiền tố REX mặc dù chúng tôi sao chép và so sánh các con trỏ.
setc [rdi]
có thể ghi trực tiếp vào bộ nhớ, nhưng dự trữ byte ECX của không gian ngăn xếp sẽ tốn nhiều kích thước mã hơn mức tiết kiệm. (Và chúng ta cần di chuyển qua mảng đầu ra. [rdi+rcx]
Cần thêm một byte cho chế độ địa chỉ, nhưng thực sự chúng ta cần một bộ đếm không cập nhật cho các ký tự được lọc để nó còn tệ hơn thế.)
Đây là nguồn YASM / NASM với các %if
điều kiện. Nó có thể được xây dựng với -felf32
(mã 32 bit) hoặc -felfx32
(mã 64 bit với x32 ABI) và với độ dài ẩn hoặc rõ ràng . Tôi đã thử nghiệm tất cả 4 phiên bản. Xem câu trả lời này cho một tập lệnh để xây dựng một nhị phân tĩnh từ nguồn NASM / YASM.
Để kiểm tra phiên bản 64 bit trên máy mà không hỗ trợ x32 ABI, bạn có thể thay đổi con trỏ regs thành 64 bit. (Sau đó, chỉ cần trừ số lượng tiền tố REX.W = 1 (0x48 byte) khỏi số đếm. Trong trường hợp này, 4 lệnh cần tiền tố REX để hoạt động trên regs 64 bit). Hoặc chỉ cần gọi nó bằng rsp
và con trỏ đầu vào trong không gian địa chỉ 4G thấp.
%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.
Tôi nhìn vào lộn xộn với DF (cờ chỉ đường điều khiển lodsd
/ scasd
vân vân), nhưng dường như nó không phải là một chiến thắng. Các ABI thông thường yêu cầu DF bị xóa khi nhập và thoát chức năng. Giả sử đã xóa khi nhập cảnh nhưng để nó được đặt khi thoát sẽ là gian lận, IMO. Sẽ thật tuyệt khi sử dụng LODSD / SCASD để tránh 3 byte sub esi, 4
, đặc biệt trong trường hợp không có rác thải cao.
Chiến lược bitmap thay thế (cho các chuỗi có độ dài ẩn x86-64)
Hóa ra điều này không lưu bất kỳ byte nào, bởi vì bt r32,r32
vẫn hoạt động với lượng rác cao trong chỉ mục bit. Nó chỉ không được ghi lại theo cách shr
này.
Thay vì bt / sbb
lấy bit vào / ra CF, hãy sử dụng shift / mask để tách bit chúng ta muốn khỏi bitmap.
%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
Vì điều này tạo ra 0/1 trong AL ở cuối (thay vì 0 / 0xFF), chúng ta có thể thực hiện đảo ngược cần thiết của giá trị trả về ở cuối hàm với xor al, 1
(2B) thay vì dec eax
(cũng là 2B trong x86-64) vẫn tạo ra một giá trị thích hợp bool
/_Bool
trả lại.
Điều này được sử dụng để tiết kiệm 1B cho x86-64 với các chuỗi có độ dài ẩn, bằng cách tránh việc không cần các byte cao của EAX. (Tôi đã sử dụng and eax, 0x7F ^ 0x20
để ép buộc chữ hoa và bằng 0 phần còn lại của eax với 3 byte and r32,imm8
. Nhưng bây giờ tôi đang sử dụng mã hóa 2 byte ngay lập tức với hầu hết các lệnh 8086, giống như tôi đã làm cho sub
và cmp
.)
Nó bị mất đến bt
/ salc
ở chế độ 32 bit và các chuỗi có độ dài rõ ràng cần ECX cho số đếm để điều này cũng không hoạt động ở đó.
Nhưng sau đó tôi nhận ra rằng mình đã sai: bt edx, eax
vẫn hoạt động với lượng rác cao trong eax. Nó dường như mặt nạ ca đếm theo cùng một cách shr r32, cl
nào (chỉ nhìn vào 5 bit thấp của cl). Điều này khác với bt [mem], reg
, có thể truy cập bên ngoài bộ nhớ được tham chiếu bởi chế độ / kích thước địa chỉ, coi nó như một chuỗi bit. (Điên rồ ...)
Hướng dẫn sử dụng nội bộ của Intel không ghi lại việc che giấu, vì vậy có lẽ đó là hành vi không có giấy tờ mà Intel đang bảo tồn cho đến bây giờ. (Đó là loại điều là không phải là hiếm. bsf dst, src
Với src = 0 luôn lá dst chưa sửa đổi, mặc dù đó là tài liệu rời dst giữ một giá trị không xác định trong trường hợp đó. AMD thực sự tài liệu src = 0 hành vi.) Tôi đã thử nghiệm trên Skylake và Core2, và bt
phiên bản hoạt động với rác khác không trong EAX bên ngoài AL.
Một mẹo nhỏ ở đây là sử dụng xchg eax,ecx
(1 byte) để đếm số vào CL. Thật không may, BMI2 shrx eax, edx, eax
là 5 byte, so với chỉ 2 byte cho shr eax, cl
. Việc sử dụng bextr
cần 2 byte mov ah,1
(đối với số bit cần trích xuất), do đó, lại là 5 + 2 byte như SHRX + AND.
Mã nguồn đã trở nên khá lộn xộn sau khi thêm các %if
điều kiện. Đây là sự phân tách các chuỗi có độ dài ẩn x32 (sử dụng chiến lược thay thế cho bitmap, vì vậy nó vẫn là 46 byte).
Sự khác biệt chính từ phiên bản độ dài rõ ràng là trong vòng lặp đầu tiên. Lưu ý làm thế nào có một lods
trước nó, và ở dưới cùng, thay vì chỉ một ở đầu vòng lặp.
; 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