c++ - saber - ¿Usaría num% 2 o num & 1 para verificar si un número es par?
numero par o impar programacion en c (12)
Ambos enfoques no son obvios, especialmente para alguien que es nuevo en la programación. Debe definir una función en inline
con un nombre descriptivo. El enfoque que use en él no importará (es probable que las micro optimizaciones no hagan que su programa sea más rápido).
De todos modos, creo que 2) es mucho más rápido ya que no requiere una división.
Bueno, hay al menos dos formas de 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 a nivel de bits (como el logial, y aquí) son mucho más eficientes que una operación de mod (o div). Por supuesto, puede argumentar que algunos compiladores 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 programadores menos experimentados. A lo que respondería es que probablemente solo beneficiará a todos si estos programadores 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.
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.
Cualquier compilador 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.
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 por un doble, sin embargo, la operación a nivel de bits no lo haría.
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 compilador.
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 compilador 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 programació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 dos veces cómo se define la operación en sus detalles (por ejemplo, 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 nuevos programadores los necesitarán de todos modos).
Pasé años insistiendo en que cualquier compilador razonable que valga la pena el espacio que consume en el disco optimizaría num % 2 == 0
a num & 1 == 0
. Luego, analizando el desmontaje 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 virtualmente equivalentes ya sea compilando 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 compilador interactivo de Matt Godbolt , ICC 13.0.1 también desafía mis expectativas.
Claro, estos compiladores 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 mala" que se debe hacer 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 regularmente apuntas 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;
}
Primero codifico la legibilidad, así que mi elección aquí es num % 2 == 0
. Esto es mucho más claro que num & 1 == 0
. Dejaré que el compilador se preocupe por la optimización para mí y solo lo ajustaré si el perfil muestra que esto es 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 programadores menos experimentados. A lo que respondería es que probablemente solo beneficiará a todos si estos programadores tardan un poco en entender las declaraciones de este tipo.
Estoy en desacuerdo. Todos deberíamos estar programados para que nuestra intención sea más clara. Si estamos probando la uniformidad, el código debería expresar eso (y un comentario debería 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
. Entonces deberíamos ver if(isEven(someNumber)) { // details }
y solo ver num % 2 == 0
una vez en la definición de isEven
.
Se ha mencionado varias veces que cualquier compilador 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é compilar 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).
Programa el primero:
int main(int argc, char **argv) {
return (argc%2==0) ? 0 : 1;
}
Resultado:
; ModuleID = ''/tmp/webcompile/_27244_0.bc''
target datalayout = "e-p: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 ; <i32> [#uses=1]
ret i32 %0
}
Programa el segundo:
int main(int argc, char **argv) {
return ((argc&1)==0) ? 0 : 1;
}
Resultado:
; ModuleID = ''/tmp/webcompile/_27375_0.bc''
target datalayout = "e-p: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 ; <i32> [#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
Si va a decir que algunos compiladores no optimizarán %2
, entonces también debe tener en cuenta que algunos compiladores utilizan 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 compiladores", o el código que está mal en "algunos compiladores"? No necesariamente los mismos compiladores 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. Creo que, por ejemplo, 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 compilador 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 ++ usted está completamente a merced de la capacidad del compilador para optimizar. Los contenedores y algoritmos estándar tienen n niveles de direccionamiento indirecto, la mayoría de los cuales desaparecen cuando el compilador ha terminado de integrarse y optimizar. Un compilador decente de C ++ puede manejar la aritmética con valores constantes antes del desayuno, y un compilador no decente de C ++ producirá código de basura sin importar lo que haga.
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 de máquina "obvias" y hace las conclusiones de desempeño 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 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 compilador de C ++ en 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 compilador 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 compilador genere Código absolutamente idéntico para ambas expresiones. No hace falta agregar que el rendimiento será exactamente el mismo.
Por cierto, esto no se llama "optimización". La optimización, por definición, es cuando el compilador decide desviarse del comportamiento estándar de la máquina abstracta de C ++ para generar el código más eficiente (preservando el comportamiento observable del programa). 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 a nivel de bits ni de división, sino de alguna otra instrucción específica dedicada a 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 manera más preferible de determinar la uniformidad / imparidad de un valor es ciertamente el enfoque num % 2
, ya que implementa el control 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 desee 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.
Todo depende del contexto. De hecho, prefiero el enfoque & 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!