¿Usaría num% 2 o num & 1 para verificar si un número es par?

Bueno, hay al menos dos formas de bajo nivel para determinar si un número dado es par o no:

1. if (num%2 == 0) { /* even */ } 2. if ((num&1) == 0) { /* even */ } 

Considero que la segunda opción es mucho más elegante y significativa, y esa es la que suelo usar. Pero no es solo una cuestión de gusto; El rendimiento real puede variar: por lo general, las operaciones bitwise (como el logial y aquí) son mucho más eficientes que una operación mod (o div). Por supuesto, puede argumentar que algunos comstackdores podrán optimizarlo de todos modos, y estoy de acuerdo … pero otros no.

Otro punto es que el segundo podría ser un poco más difícil de comprender para los progtwigdores menos experimentados. A lo que respondería es que probablemente solo beneficiará a todos si estos progtwigdores tardan un poco en entender las declaraciones de este tipo.

¿Qué piensas?

Los dos fragmentos de código dados son correctos solo si num es un int sin signo o un número negativo con una representación de complemento a dos. – Como afirman algunos comentarios del estado.

Si va a decir que algunos comstackdores no optimizarán %2 , entonces también debe tener en cuenta que algunos comstackdores usan una representación de complemento de unos para enteros con signo. En esa representación, &1 da la respuesta incorrecta para los números negativos.

Entonces, ¿qué quieres: el código que es lento en “algunos comstackdores”, o el código que está mal en “algunos comstackdores”? No necesariamente los mismos comstackdores en cada caso, pero ambos tipos son extremadamente raros.

Por supuesto, si num es de un tipo sin signo, o uno de los tipos de enteros de ancho fijo C99 ( int8_t y así sucesivamente, que deben ser el complemento de 2), entonces esto no es un problema. En ese caso, considero que %2 es más elegante y significativo, y &1 es un hack que posiblemente sea necesario a veces para el rendimiento. Pienso, por ejemplo, que CPython no realiza esta optimización, y lo mismo se aplicará a los lenguajes totalmente interpretados (aunque la sobrecarga de análisis probablemente empequeñece la diferencia entre las dos instrucciones de la máquina). Sin embargo, me sorprendería encontrar un comstackdor de C o C ++ que no lo hizo en la medida de lo posible, ya que es una obviedad en el punto de emitir instrucciones, si no antes.

En general, diría que en C ++ estás completamente a merced de la capacidad de comstackción del optimizador. Los contenedores y algoritmos estándar tienen n niveles de direccionamiento indirecto, la mayoría de los cuales desaparecen cuando el comstackdor ha terminado de integrarse y optimizar. Un comstackdor decente de C ++ puede manejar la aritmética con valores constantes antes del desayuno, y un comstackdor no decente de C ++ producirá código de basura sin importar lo que haga.

Primero codifico la legibilidad, por lo que mi elección aquí es num % 2 == 0 . Esto es mucho más claro que num & 1 == 0 . Dejaré que el comstackdor se preocupe por la optimización para mí y solo lo ajustaré si la generación de perfiles muestra que se trata de un cuello de botella. Cualquier otra cosa es prematura.

Considero que la segunda opción es mucho más elegante y significativa.

Estoy totalmente en desacuerdo con esto. Un número es par porque su congruencia módulo dos es cero, no porque su representación binaria termine con un bit determinado. Las representaciones binarias son un detalle de implementación. Confiar en los detalles de la implementación es generalmente un olor de código. Como han señalado otros, las pruebas de LSB fallan en máquinas que usan representaciones de complemento.

Otro punto es que el segundo podría ser un poco más difícil de comprender para los progtwigdores menos experimentados. A lo que respondería es que probablemente solo beneficiará a todos si estos progtwigdores tardan un poco en entender las declaraciones de este tipo.

Estoy en desacuerdo. Todos deberíamos estar progtwigdos para que nuestra intención sea más clara. Si estamos probando la uniformidad, el código debe express eso (y un comentario debe ser innecesario). Nuevamente, las pruebas de congruencia módulo dos expresan más claramente la intención del código que verificar el LSB.

Y, lo que es más importante, los detalles deben ocultarse en un método isEven . Así que deberíamos ver if(isEven(someNumber)) { // details } y solo ver num % 2 == 0 una vez en la definición de isEven .

Defino y uso una función “IsEven” para no tener que pensar en ello, luego elegí un método u otro y olvido cómo verifico si algo está parejo.

El único problema es que solo digo que con la operación a nivel de bits, estás asumiendo algo acerca de la representación de los números en binario, con módulo no. Estás interpretando el número como un valor decimal. Esto está prácticamente garantizado para trabajar con enteros. Sin embargo, considere que el módulo funcionaría para un doble, sin embargo, la operación a nivel de bits no lo haría.

Su conclusión sobre el rendimiento se basa en la premisa falsa popular.

Por alguna razón, usted insiste en traducir las operaciones del lenguaje en sus homólogos “obvios” de la máquina y hacer las conclusiones de rendimiento basadas en esa traducción. En este caso particular, llegó a la conclusión de que se debe implementar una operación bitwise y & operación de lenguaje C ++ mediante una operación bitwise y de máquina, mientras que una operación de módulo % debe implicar de alguna manera la división de la máquina, que supuestamente es más lenta. Tal enfoque es de uso muy limitado, si lo hay.

En primer lugar, no puedo imaginar un comstackdor de C ++ de la vida real que interprete las operaciones del lenguaje de una manera tan “literal”, es decir, asignándolas a las operaciones de la máquina “equivalentes”. Sobre todo porque es más frecuente de lo que cree que las operaciones de la máquina equivalente simplemente no existen.

Cuando se trata de operaciones básicas con una constante inmediata como operando, cualquier comstackdor que se respete siempre “entenderá” de inmediato que num & 1 y num % 2 para num integral hacen exactamente lo mismo, lo que hará que el comstackdor genere Código absolutamente idéntico para ambas expresiones. No hace falta agregar, el rendimiento será exactamente el mismo.

Por cierto, esto no se llama “optimización”. La optimización, por definición, es cuando el comstackdor decide desviarse del comportamiento estándar de la máquina C ++ abstracta para generar el código más eficiente (preservando el comportamiento observable del progtwig). No hay desviación en este caso, lo que significa que no hay optimización.

Además, es bastante posible que en la máquina dada, la forma más óptima de implementar ambos no sea ni a nivel de bits ni de división, sino alguna otra instrucción específica específica para la máquina. Además de eso, es muy posible que no haya ninguna necesidad de ninguna instrucción, ya que la uniformidad / rareza de un valor específico podría estar expuesta “gratis” a través de las banderas de estado del procesador o algo así como ese.

En otras palabras, el argumento de eficiencia no es válido.

En segundo lugar, para volver a la pregunta original, la forma más preferible de determinar la uniformidad / imparidad de un valor es ciertamente el enfoque num % 2 , ya que implementa el cheque requerido literalmente (“por definición”), y claramente Expresa el hecho de que el cheque es puramente matemático. Es decir, deja claro que nos importa la propiedad de un número , no la propiedad de su representación (como sería en el caso de la variante num & 1 ).

La variante num & 1 debe reservarse para situaciones en las que desea acceder a los bits de representación de valor de un número. El uso de este código para el control de ness / odd-ness es una práctica altamente cuestionable.

Se ha mencionado varias veces que cualquier comstackdor moderno crearía el mismo ensamblaje para ambas opciones. Esto me recordó a la página de demostración de LLVM que vi en otro lugar el otro día, así que pensé en intentarlo. Sé que esto no es mucho más que anecdótico, pero confirma lo que esperaríamos: x%2 x&1 se implementan de manera idéntica.

También intenté comstackr ambos con gcc-4.2.1 ( gcc -S foo.c ) y el ensamblaje resultante es idéntico (y se pega al final de esta respuesta).

Progtwig el primero:

 int main(int argc, char **argv) { return (argc%2==0) ? 0 : 1; } 

Resultado:

 ; ModuleID = '/tmp/webcompile/_27244_0.bc' target datalayout = "ep:32:32:32-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:32:64-f32:32:32-f64:32:64-v64:64:64-v128:128:128-a0:0:64-f80:32:32" target triple = "i386-pc-linux-gnu" define i32 @main(i32 %argc, i8** nocapture %argv) nounwind readnone { entry: %0 = and i32 %argc, 1 ;  [#uses=1] ret i32 %0 } 

Progtwig el segundo:

 int main(int argc, char **argv) { return ((argc&1)==0) ? 0 : 1; } 

Resultado:

 ; ModuleID = '/tmp/webcompile/_27375_0.bc' target datalayout = "ep:32:32:32-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:32:64-f32:32:32-f64:32:64-v64:64:64-v128:128:128-a0:0:64-f80:32:32" target triple = "i386-pc-linux-gnu" define i32 @main(i32 %argc, i8** nocapture %argv) nounwind readnone { entry: %0 = and i32 %argc, 1 ;  [#uses=1] ret i32 %0 } 

Salida GCC:

 .text .globl _main _main: LFB2: pushq %rbp LCFI0: movq %rsp, %rbp LCFI1: movl %edi, -4(%rbp) movq %rsi, -16(%rbp) movl -4(%rbp), %eax andl $1, %eax testl %eax, %eax setne %al movzbl %al, %eax leave ret LFE2: .section __TEXT,__eh_frame,coalesced,no_toc+strip_static_syms+live_support EH_frame1: .set L$set$0,LECIE1-LSCIE1 .long L$set$0 LSCIE1: .long 0x0 .byte 0x1 .ascii "zR\0" .byte 0x1 .byte 0x78 .byte 0x10 .byte 0x1 .byte 0x10 .byte 0xc .byte 0x7 .byte 0x8 .byte 0x90 .byte 0x1 .align 3 LECIE1: .globl _main.eh _main.eh: LSFDE1: .set L$set$1,LEFDE1-LASFDE1 .long L$set$1 ASFDE1: .long LASFDE1-EH_frame1 .quad LFB2-. .set L$set$2,LFE2-LFB2 .quad L$set$2 .byte 0x0 .byte 0x4 .set L$set$3,LCFI0-LFB2 .long L$set$3 .byte 0xe .byte 0x10 .byte 0x86 .byte 0x2 .byte 0x4 .set L$set$4,LCFI1-LCFI0 .long L$set$4 .byte 0xd .byte 0x6 .align 3 LEFDE1: .subsections_via_symbols 

Todo depende del contexto. De hecho, prefiero el enfoque de & 1 si es un contexto de sistema de bajo nivel. En muchos de estos tipos de contextos, “es par” básicamente significa que tiene poco bit cero para mí, en lugar de ser divisible por dos.

SIN EMBARGO: Tu liner tiene un error.

Tienes que ir

 if( (x&1) == 0 ) 

no

 if( x&1 == 0 ) 

Los últimos ANDs x con 1 == 0, es decir, ANDs x con 0, lo que arroja 0, lo que siempre se evalúa como falso, por supuesto.

Entonces, si lo hiciste exactamente como sugieres, ¡todos los números son impares!

Cualquier comstackdor moderno optimizará la operación de módulo, por lo que la velocidad no es una preocupación.

Yo diría que usar módulo haría las cosas más fáciles de entender, pero crear una función is_even que use el método x & 1 te da lo mejor de ambos mundos.

Ambos son bastante intuitivos.

Daría una ligera ventaja a num % 2 == 0 , pero realmente no tengo una preferencia. Ciertamente, en lo que respecta al rendimiento, es probable que sea una microoptimización, por lo que no me preocuparía por eso.

Pasé años insistiendo en que cualquier comstackdor razonable que valga la pena el espacio que consume en el disco optimizaría num % 2 == 0 a num & 1 == 0 . Luego, analizando el desassembly por una razón diferente, tuve la oportunidad de verificar realmente mi suposición.

Resulta que estaba equivocado. Microsoft Visual Studio , hasta la versión 2013, genera el siguiente código de objeto para num % 2 == 0 :

  and ecx, -2147483647 ; the parameter was passed in ECX jns SHORT $IsEven dec ecx or ecx, -2 inc ecx $IsEven: neg ecx sbb ecx, ecx lea eax, DWORD PTR [ecx+1] 

Sí, en efecto. Esto está en modo de lanzamiento, con todas las optimizaciones habilitadas. Obtienes resultados prácticamente equivalentes ya sea comstackndo para x86 o x64. Probablemente no me vas a creer; Apenas me lo creí.

Hace esencialmente lo que usted esperaría para num & 1 == 0 :

 not eax ; the parameter was passed in EAX and eax, 1 

A modo de comparación, GCC (desde v4.4) y Clang (desde v3.2) hacen lo que uno esperaría, generando un código de objeto idéntico para ambas variantes. Sin embargo, según el comstackdor interactivo de Matt Godbolt , ICC 13.0.1 también desafía mis expectativas.

Claro, estos comstackdores no están equivocados . No es un error. Hay muchas razones técnicas (como se indica adecuadamente en las otras respuestas) por las que estos dos fragmentos de código no son idénticos. Y ciertamente hay un argumento de “la optimización prematura es malvada” que se realizará aquí. Por supuesto, hay una razón por la que tardé años en darme cuenta de esto, e incluso entonces me topé con eso por error.

Pero, como dijo Doug T. , probablemente es mejor definir una función IsEven en su biblioteca en algún lugar que IsEven correctamente todos estos pequeños detalles para que nunca más tenga que pensar en ellos, y mantenga su código legible. Si apuntas regularmente a MSVC, quizás definas esta función como lo he hecho:

 bool IsEven(int value) { const bool result = (num & 1) == 0; assert(result == ((num % 2) == 0)); return result; } 

Ambos enfoques no son obvios, especialmente para alguien que es nuevo en la progtwigción. Debe definir una función en inline con un nombre descriptivo. El enfoque que use en él no importará (las micro optimizaciones probablemente no harán que su progtwig sea más rápido de una manera notable).

De todos modos, creo que 2) es mucho más rápido ya que no requiere una división.

No creo que el módulo haga las cosas más legibles. Ambos tienen sentido, y ambas versiones son correctas. Y las computadoras almacenan los números en binario, así que puedes usar la versión binaria.

El comstackdor puede reemplazar la versión de módulo con una versión eficiente. Pero eso suena como una excusa para preferir el módulo.

Y la legibilidad en este caso tan especial es la misma para ambas versiones. Es posible que un lector nuevo en progtwigción ni siquiera sepa que puede usar el módulo 2 para determinar la uniformidad de un número. El lector tiene que deducirlo. ¡ Puede que ni siquiera conozca el operador de módulo !

Al deducir el significado detrás de las declaraciones, incluso podría ser más fácil leer la versión binaria:

 if( ( num & 1 ) == 0 ) { /* even */ } if( ( 00010111b & 1 ) == 0 ) { /* even */ } if( ( 00010110b & 1 ) == 0 ) { /* odd */ } 

(Utilicé el sufijo “b” solo para aclaración, no es C / C ++)

Con la versión de módulo, debe verificar cómo se define la operación en sus detalles (p. Ej., Verificar la documentación para asegurarse de que el 0 % 2 sea ​​lo que espera).

¡El binario AND es más simple y no hay ambigüedades!

Solo la precedencia del operador puede ser una trampa con los operadores binarios. Pero no debería ser una razón para evitarlos (algún día, incluso los progtwigdores nuevos los necesitarán de todos modos).

En este punto, es posible que solo esté aumentando el ruido, pero en lo que respecta a la legibilidad, la opción de módulo tiene más sentido. Si su código no es legible, es prácticamente inútil.

Además, a menos que este sea el código que se ejecutará en un sistema que está realmente limitado por los recursos (estoy pensando en un microcontrolador), no intente optimizar para el optimizador del comstackdor.