x86 32 비트 기계 코드 기능, 21 바이트
x86-64 기계 코드 기능, 22 바이트
32 비트 모드에서 1B를 절약하려면 separator = filler-1을 사용해야합니다 (예 : fill=0
및) sep=/
. 22 바이트 버전은 임의의 구분 기호 및 필러를 사용할 수 있습니다.
이것은 입력 분리기 = \n
(0xa), 출력 채우기 기 = 0
, 출력 분리기 = /
= 필러 -1 이있는 21 바이트 버전 입니다. 이 상수는 쉽게 변경할 수 있습니다.
; see the source for more comments
; RDI points to the output buffer, RSI points to the src string
; EDX holds the base
; This is the 32-bit version.
; The 64-bit version is the same, but the DEC is one byte longer (or we can just mov al,output_separator)
08048080 <str_exp>:
8048080: 6a 01 push 0x1
8048082: 59 pop ecx ; ecx = 1 = base**0
8048083: ac lods al,BYTE PTR ds:[esi] ; skip the first char so we don't do too many multiplies
; read an input row and accumulate base**n as we go.
08048084 <str_exp.read_bar>:
8048084: 0f af ca imul ecx,edx ; accumulate the exponential
8048087: ac lods al,BYTE PTR ds:[esi]
8048088: 3c 0a cmp al,0xa ; input_separator = newline
804808a: 77 f8 ja 8048084 <str_exp.read_bar>
; AL = separator or terminator
; flags = below (CF=1) or equal (ZF=1). Equal also implies CF=0 in this case.
; store the output row
804808c: b0 30 mov al,0x30 ; output_filler
804808e: f3 aa rep stos BYTE PTR es:[edi],al ; ecx bytes of filler
8048090: 48 dec eax ; mov al,output_separator
8048091: aa stos BYTE PTR es:[edi],al ;append delim
; CF still set from the inner loop, even after DEC clobbers the other flags
8048092: 73 ec jnc 8048080 <str_exp> ; new row if this is a separator, not terminator
8048094: c3 ret
08048095 <end_of_function>
; 0x95 - 0x80 = 0x15 = 21 bytes
64 비트 버전은 2 바이트 DEC 또는를 사용하여 1 바이트 더 깁니다 mov al, output_separator
. 그 외에는, 기계 코드는 두 버전에 대해 동일하지만, 일부 레지스터 이름 변경 (예 : rcx
대신 ecx
에서 pop
).
테스트 프로그램 (베이스 3)을 실행 한 샘플 출력 :
$ ./string-exponential $'.\n..\n...\n....' $(seq 3);echo
000/000000000/000000000000000000000000000/000000000000000000000000000000000000000000000000000000000000000000000000000000000/
알고리즘 :
exp *= base
모든 필러 문자에 대해 입력을 반복하십시오 . 구분 기호와 종료 0 바이트에서 exp
필러 바이트와 구분 기호를 출력 문자열에 추가하고로 재설정하십시오 exp=1
. 입력이 줄 바꿈 과 종결 자로 끝나지 않도록 보장하는 것이 매우 편리합니다 .
입력시 구분 기호 위의 모든 바이트 값 (부호없는 비교)은 필러로 처리되고 구분 기호 아래의 모든 바이트 값은 문자열 끝 마커로 처리됩니다. (0 바이트를 명시 적으로 검사 test al,al
하면 내부 루프에 의해 설정된 플래그 에서 추가 대 분기가 필요합니다).
규칙은 후행 줄 바꿈 일 때 후행 구분 기호 만 허용합니다. 내 구현은 항상 구분 기호를 추가합니다. 32 비트 모드에서 1B를 절약하려면 해당 규칙에 구분 기호 = 0xa ( '\n'
ASCII LF = 줄 바꿈), 필러 = 0xb ( '\v'
ASCII VT = 세로 탭)가 필요합니다. 그것은 인간 친화적이지 않지만 법의 서한을 만족시킵니다. (16 진수 덤프 또는
tr $'\v' x
출력이 작동하는지 확인하거나 출력 구분 기호 및 필러를 인쇄 할 수 있도록 상수를 변경 할 수 있습니다. 또한 규칙에 따라 출력에 사용되는 것과 동일한 채우기 / 채우기로 입력을 수락 할 수있는 것으로 나타났습니다 하지만 그 규칙을 어기면 얻을 수있는 것이 없습니다.).
NASM / YASM 소스. %if
테스트 프로그램에 포함 된 내용을 사용하여 32 또는 64 비트 코드로 빌드하거나 rcx를 ecx로 변경하십시오.
input_separator equ 0xa ; `\n` in NASM syntax, but YASM doesn't do C-style escapes
output_filler equ '0' ; For strict rules-compliance, needs to be input_separator+1
output_separator equ output_filler-1 ; saves 1B in 32-bit vs. an arbitrary choice
;; Using output_filler+1 is also possible, but isn't compatible with using the same filler and separator for input and output.
global str_exp
str_exp: ; void str_exp(char *out /*rdi*/, const char *src /*rsi*/,
; unsigned base /*edx*/);
.new_row:
push 1
pop rcx ; ecx=1 = base**0
lodsb ; Skip the first char, since we multiply for the separator
.read_bar:
imul ecx, edx ; accumulate the exponential
lodsb
cmp al, input_separator
ja .read_bar ; anything > separator is treated as filler
; AL = separator or terminator
; flags = below (CF=1) or equal (ZF=1). Equal also implies CF=0, since x-x doesn't produce carry.
mov al, output_filler
rep stosb ; append ecx bytes of filler to the output string
%if output_separator == output_filler-1
dec eax ; saves 1B in the 32-bit version. Use dec even in 64-bit for easier testing
%else
mov al, output_separator
%endif
stosb ; append the delimiter
; CF is still set from the .read_bar loop, even if DEC clobbered the other flags
; JNC/JNB here is equivalent to JE on the original flags, because we can only be here if the char was below-or-equal the separator
jnc .new_row ; separator means more rows, else it's a terminator
; (f+s)+f+ full-match guarantees that the input doesn't end with separator + terminator
ret
이 함수는 시그니처가있는 x86-64 SystemV ABI를 따릅니다.이 함수
void str_exp(char *out /*rdi*/, const char *src /*rsi*/, unsigned base /*edx*/);
는 끝에 마지막 포인터를 남겨 두어 출력 문자열의 길이를 호출자에게만 알려주 므로이rdi
값을 표준 통화 규칙.
xchg eax,edi
엔드 포인터를 eax 또는 rax로 반환하는 데 1 또는 2 바이트 ( )의 비용이 듭니다 . (x32 ABI를 사용하는 경우 포인터는 32 비트 만 보장됩니다. 그렇지 않으면 xchg rax,rdi
호출자가 낮은 32 비트 외부의 버퍼에 포인터를 전달하는 경우 사용해야 합니다.) 나는 이것을 버전에 포함시키지 않았습니다. 호출자가에서 값을 얻지 않고 사용할 수있는 해결 방법이 있으므로 게시하지 않고 rdi
C에서 이것을 호출 할 수 있습니다.
우리는 출력 문자열이나 아무것도 널 종료하지 않으므로 줄 바꿈 만됩니다. 이를 수정하려면 2 바이트가 xchg eax,ecx / stosb
필요 합니다. (rcx는 0에서 0입니다 rep stosb
.)
출력 문자열 길이를 찾는 방법은 다음과 같습니다.
- rdi는 반환 할 때 문자열의 과거 끝을 가리 킵니다 (호출자가 len = end-start를 수행 할 수 있음)
- 호출자는 입력에 몇 개의 행이 있었는지 알 수 있으며 줄 바꿈을 계산합니다.
- 호출자는 큰 제로 버퍼를 사용할 수 있습니다
strlen()
.
그것들은 예쁘거나 효율적이지 않습니다 (asm 호출자의 RDI 반환 값 사용 제외). 원한다면 C에서 golfed asm 함수를 호출하지 마십시오.
크기 / 범위 제한
최대 출력 문자열 크기는 가상 메모리 주소 공간 제한에 의해서만 제한됩니다. (주로 현재 x86-64 하드웨어는 가상 주소에서 48 개의 유효 비트 만 지원하며, 0 확장 대신 부호 확장으로 반으로 나눕니다. 링크 된 답변의 다이어그램을 참조하십시오 .)
32 비트 레지스터에 지수를 축적하기 때문에 각 행은 최대 2 ** 32-1 필러 바이트 만 가질 수 있습니다.
이 함수는 0에서 2 ** 32-1 사이의베이스에 대해 올바르게 작동합니다. (베이스 0에 대한 정정은 0 ^ x = 0입니다. 즉, 필러 바이트가없는 빈 줄입니다.베이스 1에 대한 올바른 값은 항상 1 ^ x = 1이므로 라인 당 1 개의 필러.)
또한 Intel IvyBridge 이상에서 엄청나게 빠릅니다. 특히 정렬 된 메모리에 큰 행이 기록되는 경우에 특히 그렇습니다. ERMSB 기능을 사용하여 CPU에서 정렬 된 포인터를 사용하여 대량으로 rep stosb
최적의 구현을 memset()
수행합니다 . 예를 들어 180 ** 4는 0.97GB이며 i7-6700k Skylake (~ 256k 소프트 페이지 오류 포함)에서 / dev / null에 쓰려면 0.27 초가 걸립니다. (리눅스에서는 / dev / null 용 장치 드라이버가 아무 데나 데이터를 복사하지 않고 그냥 반환합니다. 따라서 항상 rep stosb
메모리를 처음 만질 때 트리거되는 소프트 페이지 오류가 발생합니다. 불행히도 BSS의 배열에 투명한 거대한 페이지를 사용하지 않으면 madvise()
시스템 호출로 인해 속도가 빨라질 것입니다.)
테스트 프로그램 :
정적 바이너리를 빌드하고 ./string-exponential $'#\n##\n###' $(seq 2)
base 2와 같이 실행하십시오 .를 구현하지 않으려면 atoi
을 사용합니다 base = argc-2
. (명령 줄 길이 제한으로 엄청나게 큰베이스를 테스트 할 수 없습니다.)
이 랩퍼는 최대 1GB의 출력 문자열에 작동합니다. (거대한 문자열조차도 단일 write () 시스템 호출 만하지 만 Linux는 파이프에 쓰는 경우에도 이것을 지원합니다). 문자를 세 려면 파이프로 들어가 wc -c
거나 strace ./foo ... > /dev/null
syscall 쓰기에 대한 인수를 보는 데 사용 하십시오.
이는 RDI 리턴 값을 활용하여에 대한 문자열 길이를 인수로 계산합니다 write()
.
;;; Test program that calls it
;;; Assembles correctly for either x86-64 or i386, using the following %if stuff.
;;; This block of macro-stuff also lets us build the function itself as 32 or 64-bit with no source changes.
%ifidn __OUTPUT_FORMAT__, elf64
%define CPUMODE 64
%define STACKWIDTH 8 ; push / pop 8 bytes
%define PTRWIDTH 8
%elifidn __OUTPUT_FORMAT__, elfx32
%define CPUMODE 64
%define STACKWIDTH 8 ; push / pop 8 bytes
%define PTRWIDTH 4
%else
%define CPUMODE 32
%define STACKWIDTH 4 ; push / pop 4 bytes
%define PTRWIDTH 4
%define rcx ecx ; Use the 32-bit names everywhere, even in addressing modes and push/pop, for 32-bit code
%define rsi esi
%define rdi edi
%define rsp esp
%endif
global _start
_start:
mov rsi, [rsp+PTRWIDTH + PTRWIDTH*1] ; rsi = argv[1]
mov edx, [rsp] ; base = argc
sub edx, 2 ; base = argc-2 (so it's possible to test base=0 and base=1, and so ./foo $'xxx\nxx\nx' $(seq 2) has the actual base in the arg to seq)
mov edi, outbuf ; output buffer. static data is in the low 2G of address space, so 32-bit mov is fine. This part isn't golfed, though
call str_exp ; str_exp(outbuf, argv[1], argc-2)
; leaves RDI pointing to one-past-the-end of the string
mov esi, outbuf
mov edx, edi
sub edx, esi ; length = end - start
%if CPUMODE == 64 ; use the x86-64 ABI
mov edi, 1 ; fd=1 (stdout)
mov eax, 1 ; SYS_write (Linux x86-64 ABI, from /usr/include/asm/unistd_64.h)
syscall ; write(1, outbuf, length);
xor edi,edi
mov eax,231 ; exit_group(0)
syscall
%else ; Use the i386 32-bit ABI (with legacy int 0x80 instead of sysenter for convenience)
mov ebx, 1
mov eax, 4 ; SYS_write (Linux i386 ABI, from /usr/include/asm/unistd_32.h)
mov ecx, esi ; outbuf
; 3rd arg goes in edx for both ABIs, conveniently enough
int 0x80 ; write(1, outbuf, length)
xor ebx,ebx
mov eax, 1
int 0x80 ; 32-bit ABI _exit(0)
%endif
section .bss
align 2*1024*1024 ; hugepage alignment (32-bit uses 4M hugepages, but whatever)
outbuf: resb 1024*1024*1024 * 1
; 2GB of code+data is the limit for the default 64-bit code model.
; But with -m32, a 2GB bss doesn't get mapped, so we segfault. 1GB is plenty anyway.
이것은 특히 x86 문자열 연산에서 asm에게 아주 잘 어울리는 재미있는 도전이었습니다 . 규칙은 줄 바꿈을 처리하지 않고 입력 문자열 끝에서 종결자를 처리하지 않도록 잘 설계되었습니다.
곱셈이 반복되는 지수는 반복 더하기를 곱하는 것과 같습니다. 어쨌든 각 입력 행의 문자 수를 계산하기 위해 반복해야했습니다.
나는 one-operand mul
또는 long imul
대신을 사용하는 것을 고려 imul r,r
했지만 EAX의 암시 적 사용은 LODSB와 충돌합니다.
또한 load 및 compare 대신 SCASB를 시도했지만 SCASBxchg esi,edi
와 STOSB가 모두 EDI를 사용하기 때문에 내부 루프 전후에 필요했습니다 . 따라서 64 비트 버전은 x32 ABI를 사용해야 64 비트 포인터가 잘리지 않습니다.
STOSB를 피하는 것은 옵션이 아닙니다. 그 밖의 다른 곳은 없습니다. SCASB를 사용할 때의 이점 중 절반은 내부 루프를 떠난 후 AL = filler이므로 REP STOSB에 대한 설정이 필요 없습니다.
SCASB는 내가하고있는 것과 다른 방향으로 비교하므로 비교를 취소해야했습니다.
xchg와 scasb에 대한 나의 최선의 시도. 작동하지만 짧지는 않습니다. ( 32 비트 코드, inc
/ dec
트릭을 사용하여 필러를 구분 기호로 변경 )
; SCASB version, 24 bytes. Also experimenting with a different loop structure for the inner loop, but all these ideas are break-even at best
; Using separator = filler+1 instead of filler-1 was necessary to distinguish separator from terminator from just CF.
input_filler equ '.' ; bytes below this -> terminator. Bytes above this -> separator
output_filler equ input_filler ; implicit
output_separator equ input_filler+1 ; ('/') implicit
8048080: 89 d1 mov ecx,edx ; ecx=base**1
8048082: b0 2e mov al,0x2e ; input_filler= .
8048084: 87 fe xchg esi,edi
8048086: ae scas al,BYTE PTR es:[edi]
08048087 <str_exp.read_bar>:
8048087: ae scas al,BYTE PTR es:[edi]
8048088: 75 05 jne 804808f <str_exp.bar_end>
804808a: 0f af ca imul ecx,edx ; exit the loop before multiplying for non-filler
804808d: eb f8 jmp 8048087 <str_exp.read_bar> ; The other loop structure (ending with the conditional) would work with SCASB, too. Just showing this for variety.
0804808f <str_exp.bar_end>:
; flags = below if CF=1 (filler<separator), above if CF=0 (filler<terminator)
; (CF=0 is the AE condition, but we can't be here on equal)
; So CF is enough info to distinguish separator from terminator if we clobber ZF with INC
; AL = input_filler = output_filler
804808f: 87 fe xchg esi,edi
8048091: f3 aa rep stos BYTE PTR es:[edi],al
8048093: 40 inc eax ; output_separator
8048094: aa stos BYTE PTR es:[edi],al
8048095: 72 e9 jc 8048080 <str_exp> ; CF is still set from the inner loop
8048097: c3 ret
의 입력에 ../.../.
대해을 생성합니다 ..../......../../
. separator = newline으로 버전의 16 진수 덤프를 표시하지 않아도됩니다.
"" <> "#"~Table~#
는 3 바이트보다 짧으며"#"~StringRepeat~#
아마도 골프를 더 할 수 있습니다.