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 바이트가 걸리기 때문에 좋지 않습니다. 간단한 DECrement는 ECX훨씬 짧아 지지만 (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명령은 바이트를 추가하고 함수 호출에서 만들고 반환해야 할 때 발생하는 약간의 오버 헤드가 있습니다. 즉 , 고전적인 속도가 아니라 인라인으로 속도 와 크기 이점을 모두 생성하는 한 가지 예입니다 -공백 절충) 함수로 작성된이 코드는 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 바이트 NEG명령입니다. 솔직히 말해서, 2 바이트는 간단한 부정을 인코딩하기에는 1 바이트가 너무 많은 것처럼 보이지만, 이것이 우리가 작업해야 할 명령어 세트입니다. 해결 방법이 있습니까? 확실한! 우리 XOR가 -2이면 두 번째 NEGation을 INCrement로 바꿀 수 있습니다 .
8B C1 | mov eax, ecx
|
F7 D8 | neg eax
83 F0 FE | xor eax, -2
40 | inc eax
|
C3 | ret
(인라인 : 6 바이트; 기능 : 9 바이트)
x86 명령어 세트의 또 다른 단점 중 하나는 다목적 LEA명령어 이며 레지스터 레지스터 이동, 레지스터 레지스터 추가, 상수로 오프셋 및 단일 명령어로 모두 스케일링 할 수 있습니다!
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명령어는이 감산 1, 2에 의해 스케일링이 입력 값이 홀수 인 경우 (1)에 의해 원래의 입력 값 및 감소 추가 - 그 행을 (2 × 1 = 0 -1); 입력 값이 짝수이면 1 (2 × 1-1 = 1)이 추가됩니다.
이것은 프런트 엔드에서 많은 실행을 수행 할 수 있기 때문에 코드를 작성하는 매우 빠르고 효율적인 방법이지만, 복잡한 인코딩에 많은 시간이 걸리기 때문에 바이트 방식으로 많이 사지 않습니다. LEA교수. 이 버전은 원래 입력 값을 LEA명령 의 입력으로 유지해야하기 때문에 인라인 목적으로도 잘 작동하지 않습니다 . 따라서이 마지막 최적화 시도로 인해 실제로 이전으로 되돌아 가서 중지해야 할 시점이 될 수 있습니다.
따라서, 최종 경쟁 항목에 대해, 우리는 입력 상기의 값을 취하는 9 바이트 함수가 ECX레지스터 (세미 표준 레지스터 기반 호출 규약 32 비트 86 페이지) 및 복귀의 결과 EAX와 마찬가지로, 레지스터 ( 모든 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