x86アセンブリ、9バイト(競合するエントリ用)
高級言語でこの課題を試みている人は誰でも、生のビットを操作する本当の楽しみを逃しています。これを行う方法には非常に多くの微妙なバリエーションがありますが、それは非常識であり、考えるのはとても楽しいです。32ビットx86アセンブリ言語で考案したいくつかのソリューションを以下に示します。
これが典型的なコードゴルフの答えではないことを事前に謝罪します。繰り返し最適化(サイズ)の思考プロセスについて多くのことを取り上げます。それがより多くの聴衆にとって興味深く、教育的であることを願っていますが、もしあなたがTL; DRタイプなら、最後までスキップしても気にしないでしょう。
明白で効率的な解決策は、値が奇数か偶数かをテストし(最下位ビットを調べることで効率的に実行できます)、それに応じてn + 1またはn-1を選択することです。入力がECX
レジスタのパラメータとして渡され、結果がEAX
レジスタに返されると仮定すると、次の関数が得られます。
F6 C1 01 | test cl, 1 ; test last bit to see if odd or even
8D 41 01 | lea eax, DWORD PTR [ecx + 1] ; set EAX to n+1 (without clobbering flags)
8D 49 FF | lea ecx, DWORD PTR [ecx - 1] ; set ECX to n-1 (without clobbering flags)
0F 44 C1 | cmovz eax, ecx ; move in different result if input was even
C3 | ret
(13バイト)
しかし、コードゴルフの目的では、LEA
エンコードに3バイトかかるため、これらの命令は素晴らしいものではありません。の単純なDEC
コメントはECX
はるかに短くなります(1バイトのみ)が、これはフラグに影響するため、コードの配置方法について少し賢くする必要があります。私たちは、デクリメントを行うことができますまず、奇数/偶数のテスト第二が、その後我々は、奇数/偶数のテストの結果を反転しなければなりません。
また、条件付き移動命令をブランチに変更することができます。これにより、コードの実行が遅くなる場合があります(ブランチの予測可能性に応じて-入力が奇数と偶数の間で一貫しない場合、ブランチが遅くなります。パターン、それはより高速になります)、それは私たちに別のバイトを節約します。
実際、このリビジョンでは、1つのレジスタのみを使用して、操作全体をインプレースで実行できます。このコードをどこかにインライン化する場合、これは素晴らしいことです(そして、非常に短いので、可能性があります)。
48 | dec eax ; decrement first
A8 01 | test al, 1 ; test last bit to see if odd or even
75 02 | jnz InputWasEven ; (decrement means test result is inverted)
40 | inc eax ; undo the decrement...
40 | inc eax ; ...and add 1
InputWasEven: ; (two 1-byte INCs are shorter than one 3-byte ADD with 2)
(インライン:7バイト、関数として:10バイト)
しかし、それを機能にしたい場合はどうでしょうか?標準の呼び出し規則では、戻り値の場合と同じレジスタを使用してパラメーターを渡すことはないためMOV
、関数の先頭または末尾にレジスタ間命令を追加する必要があります。これは実質的に速度を犠牲にしませんが、2バイトを追加します。(RET
命令はバイトも追加し、関数呼び出しを行って返す必要があるため、オーバーヘッドが発生します。これは、インライン化が単なる古典的な速度ではなく、速度とサイズの両方の利点をもたらす1つの例であることを意味します-スペースのトレードオフ。)全体として、関数として記述されたこのコードは10バイトに膨れ上がります。
10バイトで他に何ができますか?パフォーマンス(少なくとも、予測可能なパフォーマンス)を重視する場合は、そのブランチを削除することをお勧めします。これは、バイト単位で同じサイズの、分岐のないビット調整ソリューションです。基本的な前提は簡単です。ビット単位のXORを使用して最後のビットを反転し、奇数の値を偶数の値に、またはその逆に変換します。ただし、奇数の入力に対してはn-1が与えられ、偶数の入力に対してはn + 1が与えられます。これは、希望とは正反対です。そのため、それを修正するために、負の値に対して操作を実行し、効果的に符号を反転させます。
8B C1 | mov eax, ecx ; copy parameter (ECX) to return register (EAX)
|
F7 D8 | neg eax ; two's-complement negation
83 F0 01 | xor eax, 1 ; XOR last bit to invert odd/even
F7 D8 | neg eax ; two's-complement negation
|
C3 | ret ; return from function
(インライン:7バイト、関数として:10バイト)
かなり滑らかです。それがどのように改善されるかを見るのは難しいです。ただし、2つの2バイトNEG
命令が注目されます。率直に言って、2バイトは単純な否定をエンコードするには1バイトが多すぎるように見えますが、それは私たちが処理しなければならない命令セットです。回避策はありますか?もちろん!我々場合はXOR
-2によって、我々は、第二置き換えることができるNEG
とエーションをINC
リーメント:
8B C1 | mov eax, ecx
|
F7 D8 | neg eax
83 F0 FE | xor eax, -2
40 | inc eax
|
C3 | ret
(インライン:6バイト、関数として:9バイト)
x86命令セットの奇妙のもう一つは、多目的LEA
命令レジスタ・レジスタ移動を行うことができ、レジスタ-レジスタに加え、定数によって相殺し、すべて1つの命令でスケーリング!
8B C1 | mov eax, ecx
83 E0 01 | and eax, 1 ; set EAX to 1 if even, or 0 if odd
8D 44 41 FF | lea eax, DWORD PTR [ecx + eax*2 - 1]
C3 | ret
(10バイト)
このAND
命令はTEST
、ビット単位のANDを実行し、それに応じてフラグを設定するという点で、以前に使用した命令に似ていAND
ますが、実際には宛先オペランドを更新します。次に、LEA
命令はこれを2でスケーリングし、元の入力値を加算し、1をデクリメントします。入力値が奇数の場合、これから1(2×0 − 1 = -1)を引きます。入力値が偶数の場合、これは1(2×1 − 1 = 1)を追加します。
実行の多くはフロントエンドで実行できるため、これはコードを記述するための非常に高速で効率的な方法ですが、複雑なコードをエンコードするのに非常に多くの時間がかかるため、バイトの面ではあまり買いませんLEA
命令。また、このバージョンは、元の入力値をLEA
命令の入力として保持する必要があるため、インライン化の目的でも機能しません。そのため、この最後の最適化の試みでは、実際には逆戻りしており、停止する時間である可能性が示唆されています。
したがって、最後の競合するエントリには、ECX
レジスタの入力値(32ビットx86の準標準のレジスタベースの呼び出し規則)を取得し、結果をEAX
レジスタに返す9バイトの関数があります(同様に)すべてのx86呼び出し規約):
SwapParity PROC
8B C1 mov eax, ecx
F7 D8 neg eax
83 F0 FE xor eax, -2
40 inc eax
C3 ret
SwapParity ENDP
MASMで組み立てる準備ができました。Cからの呼び出し:
extern int __fastcall SwapParity(int value); // MSVC
extern int __attribute__((fastcall)) SwapParity(int value); // GNU