【问题标题】:Which is better option to use for dividing an integer number by 2?哪个是整数除以 2 的更好选择?
【发布时间】:2012-05-27 17:23:40
【问题描述】:

以下哪种技术是整数除以 2 的最佳选择,为什么?

技巧一:

x = x >> 1;

技巧2:

x = x / 2;

这里x是一个整数。

【问题讨论】:

  • 如果你真的想再次将结果分配给x,这两种方式都不合适:它应该是x >>= 1x /= 2,这取决于你打算用手术。不是因为它更快(任何现代编译器都会将所有等效变体编译为相同的快速汇编),而是因为它不那么令人困惑。
  • 我不同意leftaroundabout。 - 但我认为值得注意的是,在许多编程语言中有一个名为arithmetic shift 的操作将符号位保持在适当的位置,因此可以按预期处理有符号值。语法可能类似于x = x >>> 1。另请注意,根据平台和编译器,使用移位手动优化除法和乘法可能是相当合理的。 - 考虑微控制器,例如,没有直接 ALU 支持乘法。
  • 我更喜欢x /= 2,因为x >>= 1 看起来太像monadic bind ;)
  • @leftaroundabout - 我只是认为写x = x / 2 而不是x /= 2 更易读。主观偏好可能:)
  • @HannoBinder:当然是主观的,尤其是很多习惯。 IMO,在所有算术运算符都具有⬜= 组合的语言中,应尽可能使用这些组合。它消除了噪音并强调x修改这一事实,而一般的= 运算符则建议它采用独立于旧值的全新值。 — 始终避免使用组合运算符(以便它可读,因此只知道数学运算符的人)可能也有它的意义,但是你也需要放弃非常有用的++--+= .

标签: c++ c optimization division micro-optimization


【解决方案1】:

使用最能描述您正在尝试执行的操作的操作。

  • 如果您将数字视为位序列,请使用 bitshift。
  • 如果您将其视为数值,请使用除法。

请注意,它们并不完全相同。对于负整数,它们可以给出不同的结果。例如:

-5 / 2  = -2
-5 >> 1 = -3

(ideone)

【讨论】:

  • 最初的问题对“最佳”一词也含糊不清。在速度、可读性、欺骗学生的试题等方面的“最佳”……在没有解释“最佳”的含义的情况下,这似乎是最正确的答案。
  • 在 C++03 中,两者都是为负数定义的实现,并且 可能 给出相同的结果。在 C++11 中,除法对负数的定义很好,但移位仍然是实现定义的。
  • 虽然 / 的定义是早期 C 标准中定义的实现(如果对负数进行向上或向下舍入)。它必须始终与 %(模数/余数运算符)一致。
  • “定义的实现”意味着编译器的实现者必须在几个实现选项中进行选择,通常有很大的限制。这里,一个约束是%/ 运算符对于正数和负数操作数必须一致,这样(a/b)*b+(a%b)==a 就为真,而不管ab 的符号如何。通常,作者会做出使 CPU 发挥最佳性能的选择。
  • 所以说“编译器无论如何都会将其转换为移位”的每个人都是错误的,对吧?除非编译器可以保证您处理的是非负整数(它是常量或无符号整数),否则它不能将其更改为移位
【解决方案2】:

第一个看起来像分割吗?不,如果你想分割,使用x / 2。如果可能,编译器可以对其进行优化以使用位移(这称为强度降低),如果您自己进行,这将使其成为无用的微优化。

【讨论】:

  • 许多编译器不会将除以 2 的幂转换为位移。对于有符号整数,这将是一个不正确的优化。您应该尝试查看编译器的汇编输出并亲自查看。
  • IIRC 我用它来使 CUDA 上的并行缩减更快(避免整数 div)。然而这是一年多以前的事了,我想知道现在的 CUDA 编译器有多聪明。
  • @exDM69:许多编译器即使对有符号整数也会这样做,并根据符号进行调整。玩这些东西的好工具是:tinyurl.com/6uww253
  • @exDM69:这很重要,怎么样?我说的是“如果可能”,而不是“总是”。如果优化不正确,那么手动执行并不能使其正确(另外,如前所述,GCC 足够聪明,可以找出有符号整数的正确替换)。
  • 看看维基百科页面,这显然是有争议的,但我不会称之为强度降低。强度降低是指在循环中,通过将循环中的先前值相加,从乘法减少到加法。这更像是一个窥孔优化,编译器可以非常可靠地完成。
【解决方案3】:

继续:有很多理由支持使用x = x / 2; 这里有一些:

  • 它更清楚地表达了你的意图(假设你没有处理比特旋转寄存器比特之类的东西)

  • 无论如何,编译器都会将此简化为移位操作

  • 1234563 ,那么你有一个实际的理由使用轮班)
  • 如果除法将成为更大表达式的一部分,则使用除法运算符更有可能获得正确的优先级:

    x = x / 2 + 5;
    x = x >> 1 + 5;  // not the same as above
    
  • 有符号算术可能比上面提到的优先级问题更复杂

  • 重申一下 - 无论如何编译器都会为您执行此操作。事实上,它会将除以常数转换为各种数字的一系列移位、加法和乘法,而不仅仅是 2 的幂。请参阅this question 以获取更多相关信息的链接。

简而言之,当您真正想要乘法或除法时,编写 shift 代码并不会带来任何好处,除非可能会增加引入错误的可能性。由于编译器不够聪明,无法在适当的时候将这种事情优化为转变,这已经是一辈子了。

【讨论】:

  • 值得补充的是,虽然有优先规则,但使用括号并没有什么问题。在修改一些生产代码时,我实际上看到了a/b/c*d 形式的东西(a..d 表示数字变量)而不是更易读的(a*d)/(b*c)
  • 性能和优化取决于编译器和目标。例如,我为一个微控制器做了一些工作,除非你购买商业编译器,否则任何高于 -O0 的东西都被禁用,所以编译器肯定不会将除法转换为位移。此外,在这个目标上,位移需要一个周期,除法需要 18 个周期,并且由于微控制器时钟速度非常低,这可能确实是一个明显的性能损失(但这取决于你的代码 - 你绝对应该使用 / 直到分析告诉你这是个问题!)
  • @JackManey,如果a*db*c 有可能产生溢出,那么可读性较差的形式并不等效,并且具有明显的优势。附:我同意括号是你最好的朋友。
  • @MarkRansom - 一个公平的观点(即使我在 R 代码中遇到a/b/c*d - 在溢出意味着数据存在严重错误的上下文中 - 而不是在,比如说,性能关键的 C 代码块)。
  • 代码x=x/2; 仅比x>>=1 更“清晰”,前提是x 永远不会是一个奇数的负数或者不关心逐个错误。否则x=x/2;x>>=1; 有不同的含义。如果需要的是x>>=1 计算的值,我会认为它比x = (x & ~1)/2x = (x < 0) ? (x-1)/2 : x/2 或任何其他我能想到的使用除以二的公式更清楚。同样,如果需要x/=2 计算的值,那比((x + ((unsigned)x>>31)>>1) 更清楚。
【解决方案4】:

哪个是最好的选择,为什么要将整数除以 2?

取决于您所说的最佳

如果你想让你的同事讨厌你,或者让你的代码难以阅读,我肯定会选择第一个选项。

如果您想将一个数字除以 2,请使用第二个。

两者不等价,如果数字为负数或在较大的表达式中,它们的行为不同 - 位移位的优先级低于+-,除法的优先级更高。

您应该编写代码来表达其意图。如果您关心性能,请不要担心,优化器在这些微优化方面做得很好。

【讨论】:

    【解决方案5】:

    只需使用除法(/),假设它更清晰。编译器会做相应的优化。

    【讨论】:

    • 编译器应该进行相应的优化。
    • 如果编译器没有进行相应的优化,你应该使用更好的编译器。
    • @DavidStone:在哪些处理器上可以编译器优化一个可能为负的有符号整数除以除 1 以外的任何常数,使其与移位一样高效?
    • @supercat:这是一个很好的观点。您当然可以将值存储在无符号整数中(我觉得与有符号/无符号不匹配警告相结合时的声誉要差得多),并且大多数编译器还有一种方法告诉他们在优化时假设某些事情是正确的.我更喜欢将其包装在兼容性宏中,并使用 ASSUME(x >= 0); x /= 2; 而不是 x >>= 1; 之类的东西,但这仍然是一个重要的问题。
    【解决方案6】:

    我同意您应该支持x / 2 的其他答案,因为它的意图更清晰,编译器应该为您优化它。

    但是,与 x >> 1 相比,更喜欢 x / 2 的另一个原因是,如果 x 是带符号的 int 并且为负数,则 >> 的行为取决于实现。

    来自 ISO C99 标准的第 6.5.7 节第 5 条:

    E1 >> E2 的结果是E1 右移了E2 位位置。如果E1 有 一个无符号类型,或者如果E1 有一个有符号类型和一个非负值, 结果的值是E1/的商的整数部分 2E2。如果E1 有带符号类型和负值,则结果值 是实现定义的。

    【讨论】:

    • 值得注意的是,许多实现为 x>>scalepower 在负数上定义的行为正是将一个值除以 2 的幂以用于屏幕渲染等目的时所需要的行为,同时使用x/scalefactor 将是错误的,除非对负值进行更正。
    【解决方案7】:

    x / 2 更清晰,x >> 1 并没有快多少(根据微基准,Java JVM 快了大约 30%)。正如其他人所指出的,对于负数,舍入略有不同,因此在处理负数时必须考虑这一点。一些编译器可能会自动将x / 2 转换为x >> 1,如果他们知道这个数字不能是负数(甚至认为我无法验证这一点)。

    即使x / 2 也可能不会使用(慢)除法CPU 指令,因为some shortcuts are possible,但它仍然比x >> 1 慢。

    (这是一个 C / C++ 问题,其他编程语言有更多的运算符。对于 Java 也有无符号右移,x >>> 1,这又是不同的。它允许正确计算平均值(平均值)两个值,因此即使ab 的值非常大,(a + b) >>> 1 也会返回平均值。例如,如果数组索引可以变得非常大,则对于二进制搜索来说这是必需的。有 a bug in many versions of binary search,因为他们使用(a + b) / 2 来计算平均值。这不能正常工作。正确的解决方案是改用(a + b) >>> 1。)

    【讨论】:

    • x 可能为负数的情况下,编译器无法将x/2 转换为x>>1。如果想要的是 x>>1 将计算的值,那几乎肯定会比任何涉及 x/2 的计算相同值的表达式更快。
    • 你是对的。如果编译器知道该值不是负数,则编译器只能将x/2 转换为x>>1。我会尝试更新我的答案。
    • 编译器仍然通过将x/2 转换为(x + (x<0?1:0)) >> 1 来避免div 指令(其中>> 是算术右移,以符号位移动)。这需要 4 条指令:复制值、shr(仅获取 reg 中的符号位)、添加、sar。 goo.gl/4F8Ms4
    • 问题被标记为 C 和 C++。
    【解决方案8】:

    克努斯说:

    过早的优化是万恶之源。

    所以我建议使用x /= 2;

    这样代码很容易理解,而且我认为这种形式的操作优化,对处理器来说并没有太大的区别。

    【讨论】:

    • 如果希望整数支持 (n+d)/ d = (n/d)+1?缩放图形时违反公理将导致结果中出现可见的“接缝”。如果一个人想要一个均匀且几乎关于零对称的东西,(n+8)>>4 可以很好地工作。您能否提供任何不使用带符号右移的清晰或高效的方法?
    【解决方案9】:

    查看编译器输出以帮助您做出决定。我在 x86-64 上运行了这个测试
    gcc (GCC) 4.2.1 20070719 [FreeBSD]

    另见compiler outputs online at godbolt

    您看到的是编译器在这两种情况下都使用sarl(算术右移)指令,因此它确实识别出两个表达式之间的相似性。如果使用除法,编译器还需要针对负数进行调整。为此,它将符号位向下移动到最低位,并将其添加到结果中。与除法相比,这解决了在移动负数时的非一问题。
    由于除法案例进行 2 次移位,而显式移位情况仅进行 1 次移位,我们现在可以在此处解释由其他答案衡量的一些性能差异。

    带有汇编输出的 C 代码:

    对于除法,您的输入将是

    int div2signed(int a) {
      return a / 2;
    }
    

    编译成

        movl    %edi, %eax
        shrl    $31, %eax
        addl    %edi, %eax
        sarl    %eax
        ret
    

    换档也是如此

    int shr2signed(int a) {
      return a >> 1;
    }
    

    带输出:

        sarl    %edi
        movl    %edi, %eax
        ret
    

    【讨论】:

    • 取决于一个人在做什么,它可能会修复一个错误,或者它可能导致一个错误(与实际需要的相比) ) 这将需要使用进一步的代码来修复它。如果一个人想要的是一个下限的结果,那么右移比我所知道的任何替代方法都更快、更容易。
    • 如果您需要地板,您不太可能将您想要的内容描述为“除以 2”
    • 自然数和实数的除法都支持 (n+d)/d = (n/d)+1 的公理。实数除法也支持 (-n)/d = -(n/d),这是一个对自然数毫无意义的公理。不可能有一个对整数封闭并同时支持这两个公理的除法运算符。在我看来,说第一个公理应该适用于所有数字而第二个公理只适用于实数似乎比说第一个公理应该适用于整数或实数而不适用于整数更自然。此外,我很好奇第二个公理在什么情况下真正有用
    • 满足第一个公理的整数除法方法会将数轴划分为大小为d 的区域。这种划分对许多目的很有用。即使人们宁愿在 0 和 -1 之间以外的地方设置断点,添加偏移量也会移动它。满足第二个公理的整数除法会将数轴划分为大部分大小为d 的区域,但其中一个区域的大小为2*d-1。不完全“相等”的划分。你能提供关于oddball分区何时真正有用的建议吗?
    • 您的 shr2signed 编译器输出错误。 x86 上的 gcc 选择使用算术移位实现有符号整数的 >> (sar)。 goo.gl/KRgIkb。此邮件列表帖子 (gcc.gnu.org/ml/gcc/2000-04/msg00152.html) 确认 gcc 历来对有符​​号整数使用算术移位,因此 FreeBSD gcc 4.2.1 不太可能使用无符号移位。我更新了您的帖子以解决该问题,并且前面的段落说两者都使用了 shr,而实际上他们都使用了 SAR。 SHR 是它为/ 案例提取符号位的方式。还包括一个神螺栓链接。
    【解决方案10】:

    只是一个补充说明 -

    x *= 0.5 在某些基于 VM 的语言中通常会更快 - 特别是 actionscript,因为不必检查变量是否被 0 整除。

    【讨论】:

    • @minitech:这是一个糟糕的测试。测试中的所有代码都是不变的。在代码被 JITed 之前,它会消除所有的常量。
    • @M28:我很确定 jsPerf 的内部结构(即eval)每次都会重新发生这种情况。无论如何,是的,这是一个非常糟糕的测试,因为它是一个非常愚蠢的优化。
    【解决方案11】:

    使用x = x / 2;x /= 2; 因为将来可能会有新的程序员使用它。所以他会更容易找出代码行中发生了什么。这样的优化可能大家都不知道。

    【讨论】:

      【解决方案12】:

      我说的是编程竞赛。通常,它们具有非常大的输入,其中除以 2 发生多次,并且已知输入是正数或负数。

      x>>1 将优于 x/2。我通过运行一个程序检查了 ideone.com,其中发生了超过 10^10 除以 2 的操作。 x/2 耗时近 5.5 秒,而 x>>1 耗时近 2.6 秒。

      【讨论】:

      • 对于无符号值,编译器应将x/2 优化为x>>1。对于有符号值,几乎所有实现都将x>>1 定义为具有等效于x/2 的含义,但当x 为正时可以更快地计算,并且当x 为负时与x/2 不同。
      【解决方案13】:

      我想说有几件事需要考虑。

      1. Bitshift 应该更快,因为实际上没有特殊的计算 需要移动位,但是正如所指出的,有 负数的潜在问题。如果你确定有 正数,并且正在寻找速度,那么我会推荐 位移。

      2. 除法运算符非常易于人类阅读。 因此,如果您正在寻找代码可读性,您可以使用它。笔记 编译器优化领域已经取得了长足的进步,所以让代码变得简单 阅读和理解是一种很好的做法。

      3. 取决于底层硬件, 操作可能有不同的速度。 Amdal 定律是使 常见情况快。所以你可能有可以执行的硬件 不同的操作比其他操作更快。例如,乘以 0.5 可能比除以 2 更快。(当然,如果您希望强制执行整数除法,则可能需要乘法的下限)。

      如果您追求纯粹的性能,我建议您创建一些可以执行数百万次操作的测试。对执行进行多次采样(您的样本大小)以确定哪一个在统计上最适合您的操作系统/硬件/编译器/代码。

      【讨论】:

      • “Bitshift 应该更快”。编译器将优化划分为位移
      • 我希望他们会这样做,但除非您可以访问编译器的源代码,否则您无法确定 :)
      • 如果一个实现以最常见的方式处理它并且希望处理负数的方式与 >> 所做的匹配并且与 / 所做的不匹配,我也会推荐 bitshift。
      【解决方案14】:

      就 CPU 而言,位移运算比除法运算快。 但是,编译器知道这一点,并会在可能的范围内进行适当的优化, 因此您可以以最有意义的方式进行编码,并且知道您的代码是 高效运行。但是请记住,由于前面指出的原因,unsigned int 可以(在某些情况下)比int 优化得更好。 如果您不需要有符号算术,则不要包含符号位。

      【讨论】:

        【解决方案15】:

        x = x / 2;是适合使用的代码.. 但操作取决于您自己的程序,您希望如何产生输出。

        【讨论】:

          【解决方案16】:

          让你的意图更清晰......例如,如果你想除法,使用 x / 2,并让编译器优化它以转换运算符(或其他任何东西)。

          当今的处理器不会让这些优化对您的程序的性能产生任何影响。

          【讨论】:

            【解决方案17】:

            这个问题的答案取决于你工作的环境。

            • 如果您正在开发 8 位微控制器或任何不支持乘法的硬件,则移位是预期的且司空见惯的,虽然编译器几乎肯定会将 x /= 2 转换为 x >>= 1,但除法的存在在那种环境下,符号会比使用移位来实现除法更令人惊讶。
            • 如果您在性能关键的环境或代码部分中工作,或者您的代码可以在关闭编译器优化的情况下进行编译,x >>= 1 带有解释其推理的注释可能只是为了明确目的。李>
            • 如果您不属于上述任何一种情况,只需使用x /= 2 即可使您的代码更具可读性。最好为下一个碰巧查看您的代码的程序员节省 10 秒重复执行您的 shift 操作,而不是不必要地证明您知道在没有编译器优化的情况下 shift 更有效。

            所有这些都假定为无符号整数。简单的转变可能不是您想要的签名。此外,DanielH 提出了一个很好的观点,即将 x *= 0.5 用于某些语言,如 ActionScript。

            【讨论】:

              【解决方案18】:

              mod 2,测试 = 1。不知道 c 中的语法。但这可能是最快的。

              【讨论】:

                【解决方案19】:

                一般来说右移划分:

                q = i >> n; is the same as: q = i / 2**n;
                

                这有时被用来加快程序的速度,但会以清晰度为代价。我认为你不应该这样做。编译器足够聪明,可以自动执行加速。这意味着换个班次以牺牲清晰度为代价一无所获

                看看这个page from Practical C++ Programming.

                【讨论】:

                • 如果想计算例如(x+128)>>8 将计算x 的值不接近最大值,如果不换班,怎么能简洁地做到这一点?请注意,(x+128)/256 不起作用。你知道有什么好的表达方式吗?
                【解决方案20】:

                显然,如果您正在为下一个阅读代码的人编写代码,请确保“x/2”的清晰性。

                但是,如果您的目标是速度,请尝试两种方法并计算结果的时间。 几个月前,我研究了一个位图卷积例程,其中涉及遍历整数数组并划分每个元素by 2. 我做了各种各样的事情来优化它,包括用 "x>>1" 代替 "x/2" 的老把戏。

                当我实际对两种方式进行计时时,我惊讶地发现 x/2 比 x>>1 快

                这是使用 Microsoft VS2008 C++ 并启用默认优化。

                【讨论】:

                  【解决方案21】:

                  在性能方面。 CPU 的移位操作比除法操作码要快得多。 因此除以 2 或乘以 2 等都受益于移位操作。

                  关于外观和感觉。作为工程师,我们什么时候变得如此痴迷于连美女都不用的化妆品! :)

                  【讨论】:

                    【解决方案22】:

                    X/Y 是一个正确的...和“>>”移位运算符..如果我们想要两个除以一个整数,我们可以使用 (/) 被除数运算符。移位运算符用于移位..

                    x=x/2; x/=2;我们可以这样使用..

                    【讨论】:

                      【解决方案23】:

                      虽然 x>>1 比 x/2 快,但在处理负值时正确使用 >> 有点复杂。它需要类似于以下内容:

                      // Extension Method
                      public static class Global {
                          public static int ShiftDivBy2(this int x) {
                              return (x < 0 ? x + 1 : x) >> 1;
                          }
                      }
                      

                      【讨论】:

                        猜你喜欢
                        • 1970-01-01
                        • 1970-01-01
                        • 2014-02-16
                        • 2012-10-13
                        • 1970-01-01
                        • 1970-01-01
                        • 2018-10-23
                        • 1970-01-01
                        • 2010-12-26
                        相关资源
                        最近更新 更多