Asamblea x86, 9 bytes (para entrada competitiva)
Todos los que intentan este desafío en lenguajes de alto nivel se están perdiendo la verdadera diversión de manipular bits sin procesar. Hay tantas variaciones sutiles en las formas de hacer esto, es una locura, y es muy divertido pensar en ello. Aquí hay algunas soluciones que ideé en lenguaje ensamblador x86 de 32 bits.
Me disculpo de antemano porque esta no es la típica respuesta de código de golf. Voy a divagar mucho sobre el proceso de pensamiento de la optimización iterativa (para el tamaño). Espero que sea interesante y educativo para un público más amplio, pero si eres del tipo TL; DR, no me ofenderé si saltas hasta el final.
La solución obvia y eficiente es probar si el valor es par o impar (lo que se puede hacer de manera eficiente mirando el bit menos significativo), y luego seleccionar entre n + 1 o n − 1 en consecuencia. Suponiendo que la entrada se pasa como un parámetro en el ECXregistro, y el resultado se devuelve en el EAXregistro, obtenemos la siguiente función:
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 bytes)
Pero para propósitos de código de golf, esas LEAinstrucciones no son excelentes, ya que toman 3 bytes para codificar. Un DECcomentario simple de ECXsería mucho más corto (solo un byte), pero esto afecta a las banderas, por lo que debemos ser un poco inteligentes en la forma en que organizamos el código. Podemos hacer el decremento primera , y la prueba de par / impar segundos , pero luego tenemos que invertir el resultado de la prueba de par / impar.
Además, podemos cambiar la instrucción de movimiento condicional a una bifurcación, lo que puede hacer que el código se ejecute más lentamente (dependiendo de cuán predecible sea la bifurcación; si la entrada alterna inconsistentemente entre pares e impares, una bifurcación será más lenta; si hay un patrón, será más rápido), lo que nos ahorrará otro byte.
De hecho, con esta revisión, toda la operación se puede realizar en el lugar, utilizando solo un único registro. Esto es genial si está incorporando este código en alguna parte (y es probable que lo sea, ya que es muy corto).
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)
(en línea: 7 bytes; como una función: 10 bytes)
Pero, ¿y si quisieras que fuera una función? Ninguna convención de llamada estándar utiliza el mismo registro para pasar parámetros que para el valor de retorno, por lo que deberá agregar una MOVinstrucción de registro-registro al principio o al final de la función. Esto prácticamente no tiene costo en velocidad, pero agrega 2 bytes. (La RETinstrucción también agrega un byte, y hay una sobrecarga introducida por la necesidad de realizar y regresar de una llamada de función, lo que significa que este es un ejemplo en el que la alineación produce un beneficio de velocidad y tamaño, en lugar de ser simplemente una velocidad clásica -por-espacio de compensación.) En total, escrito como una función, este código se hincha a 10 bytes.
¿Qué más podemos hacer en 10 bytes? Si nos preocupamos por el rendimiento (al menos, el rendimiento predecible ), sería bueno deshacerse de esa rama. Aquí hay una solución de ramificación de bits sin ramas que tiene el mismo tamaño en bytes. La premisa básica es simple: usamos un XOR bit a bit para voltear el último bit, convirtiendo un valor impar en uno par, y viceversa. Pero hay un inconveniente: para las entradas impares, que nos da n-1 , mientras que para las entradas pares, nos da n + 1 , exactamente opuesto a lo que queremos. Entonces, para arreglar eso, realizamos la operación en un valor negativo, volteando el signo de manera efectiva.
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
(en línea: 7 bytes; como una función: 10 bytes)
Bastante hábil; Es difícil ver cómo se puede mejorar. Sin embargo, una cosa me llama la atención: esas dos NEGinstrucciones de 2 bytes . Francamente, dos bytes parecen un byte demasiado para codificar una simple negación, pero ese es el conjunto de instrucciones con el que tenemos que trabajar. ¿Hay alguna solución? ¡Seguro! Si tenemos XOR-2, podemos reemplazar la segunda NEGacción con un INCcomentario:
8B C1 | mov eax, ecx
|
F7 D8 | neg eax
83 F0 FE | xor eax, -2
40 | inc eax
|
C3 | ret
(en línea: 6 bytes; como función: 9 bytes)
¡Otra de las rarezas del conjunto de instrucciones x86 es la LEAinstrucción multipropósito , que puede hacer un movimiento de registro-registro, una adición de registro-registro, compensación por una constante y escalar todo en una sola instrucción!
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 bytes)
La ANDinstrucción es como la TESTinstrucción que usamos anteriormente, ya que ambos hacen un AND a nivel de bit y establecen indicadores en consecuencia, pero en ANDrealidad actualizan el operando de destino. La LEAinstrucción luego escala esto en 2, agrega el valor de entrada original y disminuye en 1. Si el valor de entrada era impar, esto resta 1 (2 × 0 - 1 = −1) de él; si el valor de entrada fue par, esto le agrega 1 (2 × 1 - 1 = 1).
Esta es una forma muy rápida y eficiente de escribir el código, ya que gran parte de la ejecución se puede hacer en el front-end, pero no nos compra mucho en forma de bytes, ya que se necesitan muchos para codificar un complejo LEAinstrucción. Esta versión tampoco funciona tan bien para fines de alineación, ya que requiere que el valor de entrada original se conserve como entrada de la LEAinstrucción. Entonces, con este último intento de optimización, hemos retrocedido, sugiriendo que podría ser el momento de detenerse.
Por lo tanto, para la entrada competitiva final, tenemos una función de 9 bytes que toma el valor de entrada en el ECXregistro (una convención de llamada basada en un registro semi-estándar en x86 de 32 bits), y devuelve el resultado en el EAXregistro (como con todas las convenciones de llamadas 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
Listo para armar con MASM; llamar desde C como:
extern int __fastcall SwapParity(int value); // MSVC
extern int __attribute__((fastcall)) SwapParity(int value); // GNU