您会使用num%2或num& 1来检查数字是否均匀?

时间:2009-12-22 21:24:52

标签: c++ numbers readability low-level bitwise-operators

嗯,至少有两种低级方法可以确定给定的数字是否均匀:

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

我认为第二种选择更加优雅和有意义,而这正是我经常使用的选择。但这不仅仅是品味问题;实际性能可能会有所不同:通常按位操作(例如logial和here)比mod(或div)操作更有效。当然,您可能会争辩说有些编译器无论如何都能够优化它,我同意......但有些编译器不会。

另一点是,对于经验不足的程序员来说,第二个可能有点难以理解。关于这一点,我会回答说,如果这些程序员花很短的时间来理解这类陈述,那么它可能只会让每个人受益。

您怎么看?

只有当num是无符号整数或带有二进制补码表示的负数时,给定的两个代码段才是正确的。 - 正如一些评论所说的那样。

12 个答案:

答案 0 :(得分:74)

我首先编写了可读性代码,因此我的选择是num % 2 == 0。这比num & 1 == 0要清楚得多。我会让编译器担心我的优化,只有在分析显示这是一个瓶颈的情况下才会调整。其他任何事情都为时过早。

  

我认为第二种选择更加优雅和有意义

我强烈反对这一点。一个数字甚至是因为它​​的模2的一致性为零,不是因为它的二进制表示以某一位结尾。二进制表示是一个实现细节。依赖于实现细节通常是代码味道。正如其他人所指出的那样,在使用补码表示的机器上测试LSB失败。

  

另一点是,对于经验不足的程序员来说,第二个可能有点难以理解。关于这一点,我会回答说,如果这些程序员花很短的时间来理解这类陈述,那么它可能只会让每个人受益。

我不同意。我们都应该编码以使我们的意图更清晰。如果我们测试均匀性,代码应该表达(并且评论应该是不必要的)。同样,测试一致性模2更清楚地表达了代码的意图而不是检查LSB。

更重要的是,细节应隐藏在isEven方法中。因此,我们应该看到if(isEven(someNumber)) { // details }并且只在num % 2 == 0的定义中看到isEven一次。

答案 1 :(得分:22)

如果你要说一些编译器不会优化%2,那么你还应该注意到一些编译器对有符号整数使用一个补码表示。在该表示中,&1 为否定数字提供了错误的答案

那么你想要什么 - “某些编译器”的代码速度慢,或者“某些编译器”代码错误?在每种情况下,不一定是相同的编译器,但这两种类型都非常罕见。

当然如果num是无符号类型,或者是C99固定宽度整数类型之一(int8_t等等,它们必须是2的补码),那么这不是'一个问题。在这种情况下,我认为%2更优雅,更有意义,而&1是一种可能在某些情况下有时需要表现的黑客。我认为例如CPython不进行这种优化,完全解释的语言也是如此(尽管解析开销可能使两个机器指令之间的差异相形见绌)。但是,如果遇到可能没有这样做的C或C ++编译器,我会感到有点惊讶,因为如果不是之前发布指令的话,这是一个明智的选择。

一般来说,我会说在C ++中你完全受编译器优化能力的支配。标准容器和算法具有n级间接,其中大部分在编译器完成内联和优化后消失。一个体面的C ++编译器可以在早餐前处理具有常量值的算术,并且不管你做什么,不合适的C ++编译器都会产生垃圾代码。

答案 2 :(得分:12)

我定义并使用“IsEven”功能,所以我不必考虑它,然后我选择了一种方法或另一种方法,并忘记我如何检查是否有问题。

只有nitpick / caveat我只是说通过按位操作,你假设有关二进制数字表示的东西,你不是模数。您将数字解释为十进制值。这几乎可以保证与整数一起使用。但是请考虑模数可以用于double,但按位运算不会。

答案 3 :(得分:10)

关于表现的结论是基于流行的错误前提。

出于某种原因,您坚持将语言操作转换为“明显的”机器对应方,并根据该翻译得出性能结论。在这种特殊情况下,您得出结论,C ++语言的按位和&操作必须通过按位和机器操作来实现,而模%操作必须以某种方式涉及机器划分,据称速度较慢。如果有的话,这种方法的使用非常有限。

首先,我无法想象一个真实的C ++编译器会以这种“文字”方式解释语言操作,即通过将它们映射到“等效”机器操作。大多数情况下,你认为等效的机器操作根本就不存在。

当涉及到使用立即常量作为操作数的基本操作时,任何自尊的编译器将始终立即“理解”num & 1num % 2对于整数num执行操作完全相同的东西,这将使编译器为两个表达式生成完全相同的代码。不用说,性能将完全相同。

BTW,这不称为“优化”。根据定义,优化是指编译器决定偏离抽象C ++机器的标准行为以生成更有效的代码(保留程序的可观察行为)。在这种情况下没有偏差,这意味着没有优化。

此外,很有可能在给定的机器上实现两者的最佳方式既不是按位也不是除法,而是一些其他专用的机器专用指令。最重要的是,很可能根本不需要任何指令,因为特定值的偶数/奇数可能会通过处理器状态标志“免费”暴露或类似这一点。

换句话说,效率参数无效。

其次,要回到原始问题,确定值的偶数/奇数的更好的方法当然是num % 2方法,因为它按字面意思实现所需的检查(“按定义“),并清楚地表明支票纯粹是数学的。即它表明我们关心的是数字的属性,而不是关于表示的属性(就像num & 1变体的情况一样)。< / p>

当您想要访问数字的值表示位时,应保留num & 1变体。使用此代码进行偶数/奇数检查是一个非常值得怀疑的做法。

答案 4 :(得分:9)

很多时候,任何现代编译器都会为两个选项创建相同的程序集。这让我想起了前几天我在某处看到的LLVM demo page,所以我想我会试一试。我知道这不仅仅是轶事,但它确实证实了我们的期望:x%2x&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

答案 5 :(得分:6)

这一切都取决于背景。如果它是一个低级别的系统环境,我实际上更喜欢&amp; 1方法。在许多这样的情境中,“甚至”基本上意味着对我来说零位为零,而不是被两个整除。

但是:你的一个班轮有一个错误。

你必须去

if( (x&1) == 0 )

if( x&1 == 0 )

后者ANDs x与1 == 0,即ANDs x为0,产生0,当然总是评估为假。

所以,如果你完全按照你的建议去做,那么所有的数字都很奇怪!

答案 6 :(得分:4)

任何现代编译器都会优化模运算,因此速度不是问题。

我认为使用modulo可以让事情更容易理解,但创建一个使用is_even方法的x & 1函数可以让您获得两全其美的效果。

答案 7 :(得分:3)

他们都非常直观。

我会给num % 2 == 0略微优势,但我真的没有偏好。当然,就性能而言,它可能是微优化,所以我不担心它。

答案 8 :(得分:1)

我花了坚持认为任何合理的编译器都值得它在磁盘上占用的空间会优化num % 2 == 0num & 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也违背了我的期望。

当然,这些编译器不是错误的。这不是一个错误。有很多技术原因(正如其他答案中充分指出的那样)为什么这两段代码不相同。当然,过早的优化是邪恶的,并且#34;在这里提出的论点。当然,我花了很多年才注意到这一点,即便如此,我只是错误地偶然发现了它。

但是,like Doug T. said,最好在你的库中定义一个IsEven函数来获取所有这些小细节的正确性,这样你就不必再考虑它们了 - 并保留你的代码可读。如果你经常定位MSVC,也许你已经完成了这个功能:

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

答案 9 :(得分:0)

对于刚接触编程的人来说,这两种方法都不是特别明显。您应该使用描述性名称定义inline函数。您在其中使用的方法无关紧要(微观优化很可能不会以明显的方式使您的程序更快)。

无论如何,我相信2)速度要快得多,因为它不需要分割。

答案 10 :(得分:0)

我认为模数不会使事物更具可读性。 两者都有意义,两个版本都是正确的。计算机以二进制形式存储数字,因此您只需使用二进制版本。

编译器可以用有效版本替换模数版本。但这听起来像是更喜欢模数的借口。

在这个特殊情况下的可读性对于两个版本都是相同的。对编程不熟悉的读者可能甚至不知道你可以使用模2来确定数字的均匀性。读者必须推断它。他可能甚至不知道模运算符

当推断语句背后的含义时,甚至可以更容易阅读二进制版本:

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

(我只使用“b”后缀来澄清,而不是C / C ++)

使用模数版本,您必须仔细检查操作在其详细信息中的定义方式(例如,检查文档以确保0 % 2符合您的预期。

二进制文件AND更简单,没有歧义!

只有运算符优先级可能是二元运算符的缺陷。但它不应该成为避免它们的理由(有一天甚至新的程序员也会需要它们。)

答案 11 :(得分:-1)

此时,我可能只是增加噪音,但就可读性而言,模数选项更有意义。如果你的代码不可读,那它几乎没用。

此外,除非这是在一个真正困扰资源的系统上运行的代码(我在想微控制器),否则不要尝试优化编译器的优化器。