你会使用 num%2 还是 num&1 来检查一个数字是不是是偶数?
Posted
技术标签:
【中文标题】你会使用 num%2 还是 num&1 来检查一个数字是不是是偶数?【英文标题】:Would you use num%2 or num&1 to check if a number is even?你会使用 num%2 还是 num&1 来检查一个数字是否是偶数? 【发布时间】:2010-12-29 06:54:50 【问题描述】:好吧,至少有两种低级方法可以确定给定数字是否为偶数:
1. if (num%2 == 0) /* even */
2. if ((num&1) == 0) /* even */
我认为第二种选择更加优雅和有意义,这是我经常使用的一种。但这不仅仅是品味问题。实际性能可能会有所不同:通常按位运算(例如这里的逻辑与)比 mod(或 div)运算效率更高。当然,你可能会争辩说有些编译器无论如何都可以优化它,我同意……但有些不会。
另一点是,对于经验不足的程序员来说,第二个可能有点难以理解。我会回答说,如果这些程序员花这么短的时间来理解这种陈述,它可能只会使每个人受益。
你怎么看?
仅当num
是无符号整数或具有二进制补码表示的负数时,给定的两个 sn-ps 才是正确的。 - 正如一些 cmets 所说的那样。
【问题讨论】:
使用最容易阅读的那个。你不应该关心性能。这是编译器的工作。我敢打赌,优化后生成的程序集完全一样。 尽管 (&1) 可能不适用于负数。这取决于实现是使用 1-compliment 还是 2-compliment。 马丁,你在这两个方面都是对的。如果至少有一个参数为负,则 / 和 % 的实际结果也是实现定义的。不过,在这个用例中它很好。 C++0x 将采用整数除法总是向零舍入的 C99 规则。 我假设每个人都会讨厌我,甚至建议 if(!(n%2)); 是否有任何 1980 年之后编写的编译器不会为这两个语句生成相同的代码? (建议使用补码,当然不会,但是真的有不使用二进制补码的编译器/芯片吗?) 【参考方案1】:我首先为可读性编写代码,所以我在这里的选择是num % 2 == 0
。这比num & 1 == 0
清楚得多。我会让编译器担心为我优化,并且只有在分析显示这是一个瓶颈时才进行调整。其他任何事情都为时过早。
我认为第二种选择更加优雅和有意义
我强烈反对这一点。一个数字是偶数是因为它的模二的同余性为零,而不是因为它的二进制表示以某个位结尾。二进制表示是一个实现细节。依赖实现细节通常是一种代码味道。正如其他人所指出的,在使用反码表示的机器上测试 LSB 会失败。
另一点是,对于经验不足的程序员来说,第二个可能有点难以理解。我会回答说,如果这些程序员花这么短的时间来理解这种陈述,它可能只会使每个人受益。
我不同意。我们都应该编码以使我们的意图更清晰。如果我们正在测试均匀性,代码应该表达出来(并且注释应该是不必要的)。同样,测试模 2 的一致性比检查 LSB 更清楚地表达了代码的意图。
而且,更重要的是,细节应该隐藏在 isEven
方法中。所以我们应该看到if(isEven(someNumber)) // details
,并且在isEven
的定义中只看到一次num % 2 == 0
。
【讨论】:
确实如此。如果您正在检查最低位,我的第一个假设是您正在测试一个标志。 一个数字 is 甚至因为它的二进制表示以某个位结尾。它没有任何问题,也没有什么使这不那么真实。 @frunsi - 不,不是。即使 %2 没有余数,一个数字也是如此。我可以想到很多数字不以 LSB 结尾的实现——例如 6502 进行 16 位提取。 @frunsi:偶数的定义是能被2整除的数。也就是说,一个能被 2 整除且余数为零的数。也就是说,一个与零模二一致的数。均匀度的定义与特定基数中数字的表示无关(比如说它以0
、2
、4
、6
或 8
以十进制结尾,或 @ 987654332@ 二进制)。这是偶数的 LSB 等于 0 的定义的结果。
@Downvoter:我相信你有正当理由。我有兴趣听听。【参考方案2】:
如果您要说某些编译器不会优化%2
,那么您还应该注意,某些编译器对有符号整数使用一个补码表示。在这种表示中,&1
给出了错误的答案 为负数。
那么你想要什么——在“某些编译器”上运行缓慢的代码,或者在“某些编译器”上出现错误的代码?不一定在每种情况下都有相同的编译器,但这两种编译器都极为罕见。
当然,如果num
是无符号类型,或者是C99 固定宽度整数类型之一(int8_t
等等,它们必须是2 的补码),那么这不是问题。在这种情况下,我认为%2
更优雅、更有意义,而&1
是一种可以想象有时对于性能来说是必要的技巧。例如,我认为 CPython 没有进行这种优化,完全解释的语言也是如此(尽管解析开销可能会使两个机器指令之间的差异相形见绌)。不过,如果遇到 C 或 C++ 编译器在可能的情况下没有这样做,我会感到有些惊讶,因为如果不是在此之前,它在发出指令时是不费吹灰之力的。
一般来说,我会说在 C++ 中,您完全受制于编译器的优化能力。标准容器和算法有 n 级间接,当编译器完成内联和优化时,大部分都会消失。一个像样的 C++ 编译器可以在早餐前处理常量值的算术运算,而一个不像样的 C++ 编译器无论你做什么都会产生垃圾代码。
【讨论】:
整数表示通常由主机架构决定,而不是编译器。你可以有一个编译器编译成使用一个或两个补码的机器......编译器编写者将根据可用的硬件来决定(除非他们真的不喜欢速度)。此外,您永远不会看到其中一台机器,因为您不会为 1970 年之前制造的计算机编写代码。今天,您真正看到的唯一地方是 IP 校验和。 它由实现决定,对此我使用“编译器”作为非正式术语。编译器编写者根据目标架构做出决定。如果我们只讨论我可能会使用的实际常见编译器的功能,那么它们都会执行优化。所以“没有性能差异”就像“整数是 2 的补码”一样正确,归结为品味/风格/清晰度。 “编译器”不是“实现”的非正式术语。 是的。也许你不希望它是这样,但如果你愿意,每次我看到有人说“这取决于编译器”对于依赖于实现的东西,我都会让你知道,你可以度过余生 24 /7 纠正他们所有;-)。无论如何,在这种情况下,签名表示是依赖于实现的,正如您正确指出的那样,无论目标架构如何,编译器都可以做它想做的任何事情。一种选择可能比另一种快得多。 我真的不确定,这可能是一种懒惰。我不讨厌说,我只是懒得说。如果我严格地谈论标准,那么我会说“实施”。否则我会说“编译器”,因为那是我直接与之交互的。而且我正在重复提问者所说的话,“一些编译器无论如何都会优化它”,而不是更正确的“一些实现”。我想我现在可以比争论更快地解决它,我只是认为它没有足够的错误需要纠正;-)【参考方案3】:我定义并使用了一个“IsEven”函数,因此我不必考虑它,然后我选择了一种方法或另一种方法,忘记了如何检查某事是否是偶数。
只有 nitpick/caveat 是我只想说,通过按位运算,你假设一些关于二进制数字表示的东西,而你不是模数。您正在将数字解释为十进制值。这几乎可以保证与整数一起使用。但是考虑到模数适用于双精度数,但按位运算不会。
【讨论】:
忘记并不意味着安全。使用模数,您可能不会对负数做出任何假设,但无论如何行为是未定义的!使用所有二进制补码机器会更安全。取模可能适用于浮点,但会因不精确而产生意外结果,而按位算术未定义并导致类型错误。【参考方案4】:您关于性能的结论是基于流行的错误前提。
出于某种原因,您坚持将语言操作翻译成“显而易见的”机器对应物,并根据该翻译得出性能结论。在这种特殊情况下,您得出结论,C++ 语言的按位与&
操作必须由按位与 机器操作实现,而模%
操作必须以某种方式涉及机器除法,据称速度较慢。如果有的话,这种方法的用途非常有限。
首先,我无法想象现实生活中的 C++ 编译器会以这种“文字”方式解释语言操作,即将它们映射到“等效”机器操作。主要是因为人们通常认为等效的机器操作根本不存在。
当涉及到以直接常量作为操作数的这种基本操作时,任何自尊的编译器都会立即“理解”num & 1
和 num % 2
for integer num
做完全相同的事情,即将使编译器为两个表达式生成完全相同的代码。当然,性能会完全一样。
顺便说一句,这不称为“优化”。根据定义,优化是编译器决定偏离抽象 C++ 机器的标准行为以生成更高效的代码(保留程序的可观察行为)。在这种情况下没有偏差,这意味着没有优化。
此外,很有可能在给定的机器上实现两者的最佳方式既不是 bitwise-and 也不是 division,而是一些其他专用机器特定的操作说明。最重要的是,很可能根本不需要任何指令,因为特定值的偶数/奇数可能通过处理器状态标志或类似的东西“免费”公开那个。
换句话说,效率参数是无效的。
其次,回到最初的问题,确定值的偶数/奇数的更可取的方法当然是num % 2
方法,因为它从字面上实现了所需的检查(“按定义”) , 并清楚地表达了检查纯数学的事实。 IE。这清楚地表明我们关心的是 number 的属性,而不是它的 representation 的属性(就像num & 1
变体的情况一样)。
num & 1
变体应保留用于需要访问数字的值表示位的情况。使用此代码进行偶数/奇数检查是一种非常值得怀疑的做法。
【讨论】:
您在这里做了很多假设,并非所有假设都是正确的,但您的态度使您获得了-1。这是一个简单的问题,你不必暗杀 OP。 我所做的大部分陈述都过于笼统,不能称为“不正确的假设”。所以:对不起,我所说的一切都是完全正确的。如果您认为某些内容不正确,则必须更具体。至于态度,我很确定你在想象一些不存在的东西。 此外,X86 是一种架构,其中值的奇数通过 PF CPU 标志暴露,这意味着智能编译器可能根本不会生成任何指令,如果这些值是作为结果获得的最后一次操作。 首先,这是一个简单的问题,简单的答案。如果你想要它,它只是复杂的。其次,在您的上一篇文章中,您自相矛盾(我所做的大多数陈述都过于笼统,不能被称为“不正确的假设”。/“人类历史上没有 C++ 编译器”如此具体),您试图过度补偿和贬低(对于那些缺乏足够深度理解问题的人来说,这只是一个“简单的问题”)并且通常是粗鲁的,完全掩盖了您所做的任何正确陈述。我建议你照照镜子。 我 +1 了这个。关于 value 操作和 value-representation 操作操作的区别的很好的解释。它还包含“直接”参数和“你不知道 CPU”参数。【参考方案5】:已经多次提到任何现代编译器都会为这两个选项创建相同的程序集。这让我想起了前几天在某处看到的LLVM demo page,所以我想我会试一试。我知道这只是轶事,但它确实证实了我们的预期:x%2
和 x&1
的实现方式相同。
我还尝试使用 gcc-4.2.1 (gcc -S foo.c
) 编译这两者,生成的程序集是相同的(并粘贴在此答案的底部)。
编程第一个:
int main(int argc, char **argv)
return (argc%2==0) ? 0 : 1;
结果:
; 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
编程第二个:
int main(int argc, char **argv)
return ((argc&1)==0) ? 0 : 1;
结果:
; 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
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
【讨论】:
最初的问题唤醒了我病态的好奇心,我也立即对此进行了测试(在看到这个答案之前)。主体为return ((num & 1) == 0)
和 return ((num % 2) == 0)
的函数,其中 num
int
或 unsigned
(总共四个函数)都被转换为相同的汇编指令——我尝试使用 x86_64、mips 和 arm 使用 gcc
分别为 4.6.1、4.4.1、4.5.0 版本。
我建议在编译时启用优化以简化 gcc 输出。并在EH_frame1:
之后省略元数据+调试信息。您可以使用godbolt.org 尝试各种架构的 gcc/clang/ICC/MSVC。【参考方案6】:
这一切都取决于上下文。如果它是低级别的系统上下文,我实际上更喜欢 &1 方法。在许多此类上下文中,“偶数”对我来说基本上意味着低位为零,而不是可被二整除。
但是:您的一个班轮有错误。
你必须去
if( (x&1) == 0 )
不是
if( x&1 == 0 )
后者与 x 与 1==0,即它与 x 与 0,产生 0,当然它总是评估为假。
所以如果你完全按照你的建议去做,所有的数字都是奇数!
【讨论】:
我想这是%2
的原因之一:%
的优先级在 C 中更直观。
是的,我发现这是一个不符合我预期的优先规则,所以我一直在注意它。在体面的调试器之前的早期,它让我很难受,天知道要花多少小时。我注意到在我发布答案后不久,这个问题就被悄悄地编辑了。
哎呀,我很惊讶它没有被编辑为在两个表达式周围添加括号。我发现最好尽可能明确优先级,以避免让阅读代码的人猜测其含义。
我也不希望读者去猜测,但我不喜欢在优先规则友好的情况下过度加括号。在这些情况下,我使用空格显示紧密绑定。例如;如果(RANGE_LO
我没有指望注释格式会破坏我的代码缩进(在上一条注释中),对此感到抱歉。【参考方案7】:
任何现代编译器都会优化模运算,因此速度不是问题。
我想说使用模数会使事情更容易理解,但是创建一个使用 x & 1
方法的 is_even
函数可以让您两全其美。
【讨论】:
【参考方案8】:它们都非常直观。
我会给num % 2 == 0
一点优势,但我真的没有偏好。当然,就性能而言,它可能是一个微优化,所以我不会担心。
【讨论】:
【参考方案9】:我花了 年 坚持认为任何合理的编译器都值得它在磁盘上消耗的空间将num % 2 == 0
优化为num & 1 == 0
。然后,由于不同的原因分析反汇编,我有机会实际验证我的假设。
事实证明,我错了。 Microsoft Visual Studio,一直到 2013 版,为 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]
是的,确实如此。这是在发布模式下,启用了所有优化。无论是为 x86 还是 x64 构建,您都会得到几乎相同的结果。你可能不会相信我;我自己几乎不相信。
它基本上可以满足您对num & 1 == 0
的期望:
not eax ; the parameter was passed in EAX
and eax, 1
通过比较,GCC(早于 v4.4)和 Clang(早于 v3.2)执行预期的操作,生成两种变体的相同目标代码。但是,根据Matt Godbolt's interactive compiler 的说法,ICC 13.0.1 也超出了我的预期。
当然,这些编译器没有错误。这不是一个错误。有很多技术原因(正如其他答案中充分指出的那样)为什么这两个 sn-ps 代码不相同。这里肯定有一个“过早的优化是邪恶的”的论点。诚然,我花了数年时间才注意到这一点是有原因的,即便如此,我也只是无意中偶然发现了它。
但是,like Doug T. said,最好在您的库中的某个地方定义一个IsEven
函数,以正确处理所有这些小细节,这样您就不必再考虑它们了——并保持您的代码可读。如果你经常以 MSVC 为目标,也许你会像我一样定义这个函数:
bool IsEven(int value)
const bool result = (num & 1) == 0;
assert(result == ((num % 2) == 0));
return result;
【讨论】:
我想知道这些编译器版本在(x << y) | (x >> (32-y))
和 (x << y) | (x >> (31-y) >> 1)
上的表现如何?恕我直言,鉴于前者在 2009 年之前使用非迂腐设置时在 99% 的 C 编译器中工作,因此应该更改标准以强制在 n 位机器上,x>>n
必须始终为 x
或 @ 987654336@(任意选择)或以实现定义的方式捕获。我认为前者的代码在各方面都优于后者,如果不是为了在标准没有要求的情况下重新发明编译器的行为方式。
幸运的是,当前的 MSVC 不再有这个错过优化的错误。 Godbolt 只能追溯到 VS2015 (CL19.0),该问题已修复。当只检查结果是否非零时,您会认为他们会费心处理带符号整数的特殊情况 %2
。 x % 2 == 1
很难,或者像 return x % 2
必须根据符号和 2 补码的低位返回 -1、0 或 1。但是x % 2 == 0
完全等同于(x&1) == 0
在针对像 x86 这样的 2 的补码系统时。
顺便说一句,对于像 Windows fastcall 这样的 register-arg 调用约定,最好的选择是 lea eax, [ecx + 1]
在复制时翻转低位,然后 and eax,1
或 and al,1
用于代码大小如果您要返回一个狭窄的布尔值。但是 gcc/clang/MSVC/ICC 都没有发现这一点。 gcc.godbolt.org/z/ubvsfx 虽然 clang 确实选择 test dil,1
/ sete al
用于独立功能,但在内联到 main 时不会。【参考方案10】:
这两种方法都不是很明显,尤其是对于刚接触编程的人来说。您应该使用描述性名称定义 inline
函数。您在其中使用的方法无关紧要(微优化很可能不会以明显的方式使您的程序更快)。
无论如何,我相信 2) 更快,因为它不需要除法。
【讨论】:
您可以对其进行基准测试,但 (1) 也不需要除法。任何以这种方式计算它的编译器都足够原始,以至于微优化远不是你最大的问题。 如果您是编程新手,并且不知道模运算符的作用,那么您可能还在上第一节编程课。【参考方案11】:我认为模数不会使事情更具可读性。 两者都有道理,而且两个版本都是正确的。并且计算机以二进制形式存储数字,因此您可以使用二进制版本。
编译器可以用有效的版本替换模版本。但这听起来像是偏爱模数的借口。
在这种非常特殊的情况下,两个版本的可读性是相同的。刚接触编程的读者可能甚至不知道您可以使用模 2 来确定数字的偶数。读者必须推断它。他可能甚至不知道模运算符!
在推断语句背后的含义时,甚至可以更容易阅读二进制版本:
if( ( num & 1 ) == 0 ) /* even */
if( ( 00010111b & 1 ) == 0 ) /* even */
if( ( 00010110b & 1 ) == 0 ) /* odd */
(我使用“b”后缀只是为了澄清,它不是 C/C++)
对于模数版本,您必须仔细检查操作在其详细信息中是如何定义的(例如,检查文档以确保 0 % 2
是您所期望的)。
二进制AND
更简单,没有歧义!
只有运算符优先级可能是二元运算符的陷阱。但这不应该成为避免使用它们的理由(总有一天,即使是新程序员也会需要它们)。
【讨论】:
几个点:0%2 定义明确。如果您知道什么是划分,那么您的老师应该同时解释模块。可以安全地假设开发人员知道它是什么,因为我们期望最低水平的数学技能。负奇数可能不会将 LSB 设置为 1。 @Martin: 0%2 定义明确。那不是我的意思。模数和除法在学校不会同时讲解。 为了扭转你的观点,刚接触编程的读者可能不知道在二进制补码表示中,偶数的 LSB 为 0。他甚至可能不知道按位与运算符!至少模解具有反映“均匀度”数学定义的特性。 有趣的是,二进制文字已经进入 C++14:0b00010111
.【参考方案12】:
在这一点上,我可能只是增加了噪音,但就可读性而言,取模选项更有意义。如果你的代码不可读,它实际上是没有用的。
另外,除非这是在资源非常紧张的系统上运行的代码(我认为是微控制器),否则不要尝试针对编译器的优化器进行优化。
【讨论】:
以上是关于你会使用 num%2 还是 num&1 来检查一个数字是不是是偶数?的主要内容,如果未能解决你的问题,请参考以下文章
C : while( scanf("%d",&num) != 1 ) 无限循环
C语言 新手求解释! if (num[i]!=num[k]&&num[i]!=num[j]&&num[j]!=num[k])