x86 32 비트 기계 코드 기능, 42 41 바이트
현재 가장 짧은 비 골프 언어 답변, @streetster의 q / kdb + 보다 1B 짧습니다 .
진실의 경우 0, 거짓의 경우 0이 아닌 경우 : 41 40 바이트. 일반적으로 32 비트의 경우 1 바이트, 64 비트의 경우 2 바이트를 저장합니다.
암시 적 길이의 문자열 사용 (C 스타일 0으로 종료) : 45 44 바이트
x86-64 머신 코드 (x32 ABI와 같은 32 비트 포인터 포함) : 44 43 바이트 .
암시 적 길이의 문자열을 가진 x86-64, 여전히 46 바이트 (이동 / 마스크 비트 맵 전략은 현재 손익분기)
이것은 C 서명을 가진 함수입니다 _Bool dennis_like(size_t ecx, const char *esi)
. 호출 규칙은 약간 비표준이며 MS 벡터 콜 / 패스트 콜과 비슷하지만 다른 인수 레지스터가 있습니다 : ESI의 문자열과 ECX의 길이. 그것은 단지 arg-regs와 EDX를 방해합니다. AL은 SysV x86 및 x32 ABI에서 허용하는 높은 바이트를 가비지로 반환 값을 보유합니다. IDK는 bool 또는 좁은 정수를 반환 할 때 고가 쓰레기에 대해 MS의 ABI가 말한 내용입니다.
알고리즘 설명 :
입력 문자열을 반복하여 필터링하고 스택의 부울 배열로 분류하십시오. 각 바이트에 대해 알파벳 문자인지 확인하고 (아니면 다음 문자로 계속) 0-25 (AZ)의 정수로 변환하십시오. . vowel = 0 / consonant = 1의 비트 맵을 확인하려면 0-25 정수를 사용하십시오. 비트 맵은 32 비트 즉시 상수로 레지스터에로드됩니다. 비트 맵 결과에 따라 스택에 0 또는 0xFF를 푸시합니다 (실제로는 32 비트 요소의 하위 바이트에 최상위 3 바이트에 가비지가있을 수 있음).
첫 번째 루프는 0 또는 0xFF의 배열 (가비지로 채워진 dword 요소)을 생성합니다. 포인터가 중간에서 교차 할 때 (또는 홀수의 알파벳 문자가있는 경우 둘 다 동일한 요소를 가리킬 때) 중지되는 두 번째 루프로 일반적인 회문 검사를 수행하십시오. 위로 움직이는 포인터는 스택 포인터이며 POP를 사용하여로드 + 증가시킵니다. 이 루프에서 비교 / setcc 대신 두 개의 가능한 값만 있기 때문에 XOR을 사용하여 동일 / 다른 것을 감지 할 수 있습니다. 일치하지 않는 요소가 있는지 여부를 OR로 누적 할 수 있지만 XOR에서 설정 한 플래그의 초기 분기는 적어도 좋습니다.
두 번째 루프는 byte
피연산자 크기를 사용하므로 첫 번째 루프가 각 배열 요소의 하위 바이트 외부에 남겨진 쓰레기를 신경 쓰지 않습니다.
문서화되지 않은 salc
명령어 를 사용하여 동일한 방식으로 CF에서 AL을 설정 sbb al,al
합니다. Knight 's Landing까지 모든 Intel CPU (64 비트 모드 제외)에서 지원됩니다! Agner Fog는 Ryzen을 포함한 모든 AMD CPU에 대한 타이밍을 나열 하므로 x86 공급 업체가 8086 이후로이 바이트의 opcode 공간을 차지할 것을 요구하면이를 활용할 수도 있습니다.
흥미로운 속임수 :
- 결합 된 isalpha ()와 toupper ()에 대한 unsigned-compare 비교 , eax를 채우기 위해 바이트를 0으로 확장하여 다음을 설정합니다.
- 에 대한 멋진 비트 맵 출력
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 배의 메모리 사용으로 인해 많은 캐시 누락이 발생하지 않는 수 천 자 미만의 문자열에 대해서는 골프 중 어느 것도 그렇게 심하게 다 치지 않기 때문입니다. (또한 모든 문자를 반복하기 전에 데니스와 같은 문자열이 아닌 경우 초기에 걸리는 답을 잃을 수도 있습니다.) 많은 CPU (예 : 3 uops vs. Skylake에서 1) salc
보다 느리지 setcc
만 비트 맵 검사는 bt/salc
문자열 검색 또는 정규식 일치보다 여전히 빠릅니다. 또한 시작 오버 헤드가 없으므로 짧은 문자열에는 매우 저렴합니다.
한 번에 한 번에 수행하면 위아래 방향에 대한 분류 코드가 반복됩니다. 그것은 더 빠르지 만 더 큰 코드 크기입니다. (물론 빨리 원한다면 SSE2 또는 AVX2를 사용하여 한 번에 16 자 또는 32자를 수행 할 수 있지만 부호있는 범위의 맨 아래로 범위를 이동하여 비교 트릭을 계속 사용할 수 있습니다).
cmdline arg로이 함수를 호출하고 status = return value로 종료하는 테스트 프로그램 (ia32 또는 x32 Linux 용) . int80h.orgstrlen
에서 구현 .
; 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
. 이는 3 대신 3 바이트입니다 setc al
. 또한 32 비트에만 1 바이트 inc / dec r32가 있기 때문에 추가 바이트 dec
또는 not
끝에 추가 바이트가 필요합니다 . x32 ABI (긴 모드의 32 비트 포인터)를 사용하면 포인터를 복사하고 비교하더라도 REX 접두사를 피할 수 있습니다.
setc [rdi]
메모리에 직접 쓸 수는 있지만 스택 공간의 ECX 바이트를 예약하면 절약되는 것보다 코드 크기가 더 비쌉니다. (그리고 우리는 출력 배열을 통해 이동해야합니다. [rdi+rcx]
어드레싱 모드를 위해 1 바이트가 더 필요하지만 실제로 필터링 된 문자를 업데이트하지 않는 카운터가 필요하므로 그보다 나빠질 것입니다.)
%if
조건 이있는 YASM / NASM 소스입니다 . -felf32
(32 비트 코드) 또는 -felfx32
(x32 ABI가있는 64 비트 코드) 및 암시 적 또는 명시 적 길이 로 빌드 할 수 있습니다 . 4 가지 버전을 모두 테스트했습니다. NASM / YASM 소스에서 정적 바이너리를 빌드하는 스크립트는 이 답변 을 참조하십시오 .
x32 ABI를 지원하지 않는 컴퓨터에서 64 비트 버전을 테스트하려면 포인터 등록을 64 비트로 변경할 수 있습니다. (카운트에서 REX.W = 1 접두사 (0x48 바이트)의 수를 빼면됩니다.이 경우 64 비트 정규식에서 작동하려면 4 개의 명령어에 REX 접두사가 필요합니다.) 또는 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를 지워야합니다. 입국이 허가되었지만 출국시 설정된 채로두면 부정 행위가 될 것입니다. sub esi, 4
특히 쓰레기가 많지 않은 경우 3 바이트를 피하기 위해 LODSD / SCASD를 사용하는 것이 좋습니다 .
대체 비트 맵 전략 (x86-64 암시 길이 문자열의 경우)
bt r32,r32
비트 인덱스의 가비지가 여전히 높기 때문에 바이트를 저장하지 않습니다 . 방법 shr
이 문서화되어 있지 않습니다 .
bt / sbb
CF로 비트를 가져 오거나 내보내는 대신 시프트 / 마스크를 사용하여 비트 맵에서 원하는 비트를 분리하십시오.
%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
이렇게하면 (0 / 0xFF 대신) AL에서 0/1이 생성되므로 xor al, 1
( dec eax
x86-64에서 2B) 대신 (2B)를 사용 하여 함수 끝에서 반환 값을 필요한 반전으로 수행 할 수 있습니다. 여전히 올바른 bool
/_Bool
반환 값을 생성 합니다.
이것은 높은 바이트의 EAX를 0으로 만들 필요가 없으므로 암시 적 길이의 문자열로 x86-64의 1B를 절약하는 데 사용되었습니다. (나는 and eax, 0x7F ^ 0x20
3 바이트로 나머지 eax를 대문자로 강제로 제로로 and r32,imm8
사용했지만 지금은 이미했던 것처럼 대부분의 8086 명령어가 가지고있는 2 바이트 AL 코드 포함 인코딩을 사용하고 있습니다. 에 sub
와 cmp
.)
32 비트 모드에서 bt
/ salc
로 손실 되며 명시 길이 문자열은 카운트에 ECX가 필요하므로 작동하지 않습니다.
그러나 나는 내가 틀렸다는 것을 깨달았다 : bt edx, eax
여전히 도끼의 높은 쓰레기로 작동합니다. 그것은 분명히 마스크 변화는 계산 같은 방법 shr r32, cl
않습니다 (CL의 낮은 5 비트에서만보고). 이는 bt [mem], reg
주소 지정 모드 / 크기에서 참조하는 메모리 외부에서 액세스하여 비트 열로 취급 할 수있는와 다릅니다. (미친 CISC ...)
인텔의 insn set ref manual은 마스킹을 문서화하지 않으므로 인텔이 현재 보존하고있는 문서화되지 않은 동작 일 수 있습니다. (이런 경우는 드문 일이 아닙니다. 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
. 를 사용 bextr
하려면 2 바이트 mov ah,1
(추출 비트 수)가 필요하므로 SHRX + AND와 마찬가지로 5 + 2 바이트입니다.
%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