x86 32ビットマシンコード関数、42 41バイト
現在、ゴルフ言語以外の最短の回答で、@ streetsterのq / kdb +よりも1B短くなっています。
真実の場合は0、偽の場合は0以外:41 40バイト。 (一般的に、32ビットでは1バイト、64ビットでは2バイト節約されます)。
暗黙の長さの文字列(Cスタイル0で終了):45 44バイト
(X32 ABIのような32ビット・ポインタを有する)x86-64の機械コード44 43バイト。
暗黙の長さの文字列を持つx86-64、まだ46バイト(シフト/マスクビットマップ戦略は損益分岐点になりました)。
これはC署名付きの関数_Bool dennis_like(size_t ecx, const char *esi)
です。呼び出し規約はやや非標準で、MS vectorcall / fastcallに近いですが、異なる引数レジスタ(ESIの文字列とECXの長さ)があります。arg-regsとEDXのみを破壊します。ALは戻り値を保持し、上位バイトはガベージを保持します(SysV x86およびx32 ABIで許可されています。boolまたは狭い整数を返すときのMSのABIが高ガベージについて言うIDK)。
アルゴリズムの説明:
入力文字列をループし、スタック上のブール配列にフィルタリングおよび分類します。各バイトについて、アルファベット文字であるかどうかを確認し(そうでない場合は次の文字に進みます)、0-25(AZ)の整数に変換します。母音= 0 /子音= 1のビットマップをチェックするには、0〜25の整数を使用します。(ビットマップは、32ビットの即時定数としてレジスタにロードされます)。ビットマップの結果に応じて、スタックに0または0xFFをプッシュします(実際には、32ビット要素の下位バイトで、上位3バイトにゴミがある可能性があります)。
最初のループは、0または0xFFの配列を生成します(ゴミで埋められたdword要素内)。通常の回文チェックは、ポインタが中央で交差するとき(または、アルファベット文字の数が奇数の場合に両方が同じ要素を指すとき)に停止する2番目のループで行います。上に移動するポインターはスタックポインターであり、POPを使用して読み込みとインクリメントを行います。このループでは、compare / setccの代わりに、XORを使用して、2つの可能な値しかないため、同じ/異なるものを検出できます。一致しない要素が見つかったかどうかを(ORを使用して)累積できますが、XORによって設定されたフラグのアーリーアウトブランチは、少なくとも同等です。
2番目のループはbyte
operand-sizeを使用するため、最初のループが各配列要素の下位バイトの外側に残すゴミを気にしないことに注意してください。
文書化さsalc
れていない命令を使用して、同じ方法でCFからALを設定しsbb al,al
ます。Knight's Landingも含め、すべてのIntel CPU(64ビットモードを除く)でサポートされています! Agner Fogは、すべてのAMD CPU(Ryzenを含む)でのタイミングもリストしているため、x86ベンダーが8086以降、そのバイトのオペコードスペースを使用することを要求する場合は、それを利用することもできます。
興味深いトリック:
- isalpha()とtoupper()を組み合わせたunsigned-compareトリック。eaxを埋めるためにバイトをゼロ拡張し、次の設定を行います。
- 以下のためのレジスタとイミディエイトビットマップ
bt
、のためのいくつかの素晴らしいコンパイラの出力に触発されましたswitch
。
- プッシュインループでスタック上に可変サイズの配列を作成します。(asmの標準ですが、暗黙の長さの文字列バージョンではCでできることではありません)。すべての入力文字に対して4バイトのスタックスペースを使用しますが、最適なゴルフの周りで少なくとも1バイトを節約し
stosb
ます。
- ブール配列のcmp / setneの代わりに、ブール値をXORして直接真理値を取得します。(
cmp
/ salc
はsalc
CFでのみ動作し、0xFF-0はCFを設定しないため、オプションではありません sete
。3バイトですがinc
、ループの外側を回避します。2バイトの純コスト(64ビットモードで1 ))vs. xor in the loop and infix with 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
これはおそらく最速の答えの1つでもあります。少なくとも4倍のメモリ使用量が多くのキャッシュミスを引き起こさない数千文字未満の文字列の場合、ゴルフはそれほどひどく痛くないからです。(すべての文字をループする前にデニスのような文字列を早期に取得する回答も失われる可能性があります。) 多くのCPU salc
よりも遅いsetcc
(たとえば、Skylakeで1 uopに対して3 uops)が、ビットマップチェックではbt/salc
文字列検索や正規表現一致よりも高速です。また、起動時のオーバーヘッドがないため、短い文字列には非常に安価です。
オンザフライで1つのパスで行うと、上下方向の分類コードを繰り返すことになります。それは高速ですが、コードサイズが大きくなります。(もちろん、高速にしたい場合は、SSE2またはAVX2で一度に16文字または32文字を実行できますが、符号付き範囲の下部に範囲をシフトする比較トリックを使用します)。
テストプログラム(ia32またはx32 Linux用)は、cmdline argを使用してこの関数を呼び出し、status =戻り値で終了します。 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バイトではなく2バイトのみですsetc al
。それはまたのために余分なバイトが必要になりますdec
かnot
(32ビットのみが1バイトのINC] / [DEC R32を持っているので)最後に。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ビットバージョンをテストするには、ポインターregsを64ビットに変更できます。(その後、単純にカウントからREX.W = 1プレフィックス(0x48バイト)の数を減算します。この場合、64ビットregで動作するには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をクリアする必要があります。入場時にクリアされたものの、退場時にセットされたままにすると、不正行為となるIMO。sub esi, 4
特に高ガベージがない場合、LODSD / SCASDを使用して3バイトを回避するとよいでしょう。
代替ビットマップ戦略(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
これにより、最後にALで0/1が生成されるため(0 / 0xFFではなく)、関数の最後で戻り値の必要な反転をxor al, 1
(dec eax
x86-64でも2B )ではなく(2B)で行うことができますそれでも適切なbool
/_Bool
戻り値を生成します。
これは、EAXの上位バイトをゼロにする必要を回避することにより、暗黙的な長さの文字列でx86-64の1Bを節約するために使用されていました。(and eax, 0x7F ^ 0x20
3バイトでeaxの残りを強制的に大文字にしてゼロにするために使用していましたがand r32,imm8
、今ではほとんどの8086命令が持っているように、ほとんどの8086命令が持っている2バイトのALエンコードを使用しています用sub
してcmp
。)
32ビットモードではbt
/ に失われsalc
、明示的な長さの文字列はカウントにECXを必要とするため、そこでも機能しません。
しかし、それから私は自分が間違っていることに気付きました。bt edx, eax
それでも、eaxの高いゴミで動作します。それは明らかにマスクシフト数えるのと同じ方法がshr r32, cl
ない(唯一のCLの低5ビットを見て)。これはbt [mem], reg
、addressing-mode / sizeによって参照されるメモリの外部にアクセスでき、ビット文字列として扱うことができるとは異なります。(クレイジーCISC ...)
Intelのinsn set refマニュアルはマスキングを文書化していないため、Intelが現在保存している文書化されていない動作の可能性があります。(そのようなことは珍しいことではありません 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バイトで、vs.2 はわずか2バイトですshr eax, cl
。を使用bextr
するにはmov ah,1
(抽出するビット数に対して)2バイトが必要なので、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