【问题标题】:Is shifting bits faster than multiplying and dividing in Java? .NET? [closed]移位比Java中的乘法和除法更快吗? 。网? [关闭]
【发布时间】:2026-02-13 10:45:02
【问题描述】:

如果您碰巧使用 2 的幂,在大多数甚至所有 CPU 上,左右移位显然比乘法和除法运算要快。但是,对于某些阅读器和某些算法,它可能会降低代码的清晰度.位移对于性能真的有必要吗,还是我可以期望编译器或 VM 注意到这种情况并对其进行优化(特别是当 2 的幂是文字时)?我主要对 Java 和 .NET 行为感兴趣,但也欢迎深入了解其他语言实现。

【问题讨论】:

  • 为什么要问我们?试试看,你就知道了!双向编写代码,拿出秒表,看看会发生什么。
  • 一旦jvm或clr的新版本发布,答案就会失效。或者当有人在嵌入式系统上尝试它时。或者移动CPU。或者GPU。或者满月的时候。或者当行星对齐时。或者......(你明白了。)
  • @Eric 为什么要做的工作比必要的多?在 SO 上提出问题需要 1 分钟,去拿个三明治,然后回来寻找答案详细解释为什么会这样。
  • 我们的观点会比实验中的观点更快地过时。 IE。已经过时或与下一个 cpu vm 等。所以只需尽可能清晰地编写代码,以便优化器能够理解它,从而优化它。
  • 尽管下面的答案都发誓现代编译器太聪明了,不会错过这类事情,我自己在 C# 4.0 上的测试表明,对于 int i,声明 (i << 3) 是 15在发布模式下,比等效的 (i * 8) 快 % 到 25%。这在各种条件执行流程中是一致的,因此它似乎不是优化侥幸。绝对应该在性能关键的循环中测试自己。 JVM 比 .NET CLR 更优化,所以如果这种特殊的低效率仅适用于 .NET,我不会感到惊讶。

标签: c# java .net optimization bit-manipulation


【解决方案1】:

它取决于硬件。如果我们谈论的是微控制器或 i386,那么转换可能会更快,但是正如几个答案所述,您的编译器通常会为您进行优化。

在现代(Pentium Pro 及更高版本)硬件上,流水线使这完全无关紧要,偏离常规通常意味着您失去的优化比获得的多得多。

微优化不仅浪费您的时间,而且很难做到正确。

【讨论】:

    【解决方案2】:

    根据this microbenchmark的结果,移位是除法的两倍(Oracle Java 1.7.0_72)。

    【讨论】:

      【解决方案3】:

      请注意,向下移动和除法将(在 Java 中,当然)对负数和奇数给出不同的结果。

      int a = -7;
      System.out.println("Shift: "+(a >> 1));
      System.out.println("Div:   "+(a / 2));
      

      打印:

      Shift: -4
      Div:   -3
      

      由于 Java 没有任何无符号数,Java 编译器实际上不可能对此进行优化。

      【讨论】:

        【解决方案4】:

        在这些情况下,人类是错误的。

        99% 当他们尝试第二次猜测现代(以及所有未来)编译器时。
        99.9% 的人尝试同时猜测现代(以及所有未来)的 JIT。
        99.999% 当他们尝试第二次猜测现代(以及所有未来)的 CPU 优化时。

        以准确描述您想要完成的目标而不是如何完成的方式进行编程。 JIT、VM、编译器和 CPU 的未来版本都可以独立改进和优化。如果您指定的东西如此微小和具体,您将失去所有未来优化的好处。

        【讨论】:

        • ...和 ​​83.7% 的统计数据是由 :-)
        • +1 思考未来
        【解决方案5】:

        当我刚刚编写这段代码时,我惊呆了,并意识到移位实际上比乘以 2 慢!

        (编辑:根据 Michael Myers 的建议更改代码以停止溢出,但结果相同!这里有什么问题?)

        import java.util.Date;
        
        public class Test {
            public static void main(String[] args) {
                Date before = new Date();
                for (int j = 1; j < 50000000; j++) {
                    int a = 1 ;
                    for (int i = 0; i< 10; i++){
                        a *=2;
                    }
                }
                Date after = new Date();
                System.out.println("Multiplying " + (after.getTime()-before.getTime()) + " milliseconds");
                before = new Date();
                for (int j = 1; j < 50000000; j++) {
                    int a = 1 ;
                    for (int i = 0; i< 10; i++){
                        a = a << 1;
                    }
                }
                after = new Date();
                System.out.println("Shifting " + (after.getTime()-before.getTime()) + " milliseconds");
            }
        }
        

        结果是:

        乘以 639 毫秒
        移动 718 毫秒

        【讨论】:

        • 您能否对其进行基准测试以使其不会溢出?如果我没记错的话,溢出可能很昂贵。
        • 这对于没有专门为其准备的机器上的单个基准测试几乎相同。
        • 你有没有试过多次运行看看结果是否保留?
        • 续:实际的
        • 您正在为乘法案例初始化新变量,而您正在为位移案例重新使用日期变量。那里可能有 GC 的簿记。此外,如果您使用“
        【解决方案6】:

        今天的大多数编译器所做的不仅仅是将乘法或除以 2 的幂转换为移位操作。在优化时,许多编译器可以使用编译时间常数优化乘法或除法,即使它不是 2 的幂。通常,乘法或除法可以分解为一系列移位和加法,如果这一系列操作会更快而不是乘法或除法,编译器将使用它。

        对于除以常数,编译器通常可以将运算转换为乘以“幻数”,然后进行移位。这可以节省主要的时钟周期,因为乘法通常比除法运算快得多。

        Henry Warren's book, Hacker's Delight, 有很多关于这个主题的信息,在配套网站上也有很好的介绍:

        另见讨论(带有一两个链接):

        无论如何,这一切都归结为允许编译器处理微优化的繁琐细节。自从你自己的班次超越编译器以来,已经有好几年了。

        【讨论】:

        • 您应该注意,如果 x 是奇数负数,x/2x&gt;&gt;1 将计算不同的东西。如果有人关心计算将如何处理奇数负数,则应使用产生正确答案的形式。
        • 也很喜欢Hacker's Delight。不幸的是,配套网站不再可用。有兴趣的可以使用2019-07-17 capture,貌似是最后的完整截图。
        【解决方案7】:

        在我测试过的计算机上,整数除法比其他运算慢 4 到 10 倍

        当编译器可能会替换除以 2 的倍数并让您看不到任何差异时,除以非 2 的倍数时会明显变慢。

        例如,我有一个(图形)程序,其中有很多很多除以 255 的除法。 其实我的计算是:

        r = (((top.R - bottom.R) * alpha + (bottom.R * 255)) * 0x8081) >> 23;
        

        我可以确保它比我之前的计算快很多:

        r = ((top.R - bottom.R) * alpha + (bottom.R * 255)) / 255;
        

        所以不,编译器无法完成所有优化技巧。

        【讨论】:

          【解决方案8】:

          几乎任何值得一提的环境都会为您优化这一点。如果没有,你就有更大的鱼要炸。说真的,不要再浪费一秒钟的时间来思考这个问题了。当您遇到性能问题时,您会知道。在您运行分析器后,您将知道是什么原因造成的,并且应该相当清楚如何解决它。

          你永远不会听到有人说“我的应用程序太慢了,然后我开始随机将x * 2 替换为x &lt;&lt; 1 并且一切都修复了!”性能问题通常是通过找到一种方法来减少一个数量级的工作,而不是通过找到一种方法来将相同的工作提高 1%。

          【讨论】:

          • +1 表示“在你运行分析器之后...”
          • 使用更清晰的那个。如果它是乘法或除法中的常数因子,编译器几乎肯定会做正确的事情。
          • 非常好的答案 - 过早的优化几乎总是浪费时间。
          • 如果 x 可能是奇数负数,x&gt;&gt;1x/2 的行为是不同的。除非这两个操作都是正确的(例如,因为 x 永远不会是奇数负数,或者其他地方的代码将处理非一条件),否则哪个更快的问题是无关紧要的。我经常看到x/2x&gt;&gt;=1 正确时使用,反之亦然[寻找图形在原点X 和Y 坐标处有难看接缝的程序]。
          • 当人们在答案中添加自己的观点时,我通常不会欣赏它,但这只是纯粹的事实。也许位移在 90 年代可能是一种值得进行的性能优化,但现在用现代编译器的复杂性和强大功能来证明位移是一种“性能黑客”是愚蠢的。
          【解决方案9】:

          大多数编译器会在适当的时候将乘法和除法转换为位移。这是最简单的优化之一。所以,你应该做更容易阅读和适合给定任务的事情。

          【讨论】:

            【解决方案10】:

            我会问“你在做什么这很重要?”。首先设计代码的可读性和可维护性。进行位移位与标准乘法的可能性会导致性能差异非常小。

            【讨论】:

              【解决方案11】:

              如果编译器(编译时常量)或 JIT(运行时常量)知道除数或被乘数是 2 的幂并且正在执行整数运算,它会为您将其转换为移位。

              【讨论】:

                【解决方案12】:

                您几乎可以肯定地依赖于对移位操作的二次乘法优化。这是编译器构造的学生将学习的首批优化之一。 :)

                但是,我认为对此没有任何保证。您的源代码应该反映您的意图,而不是试图告诉优化器该做什么。如果要使数量更大,请使用乘法。如果您要将位域从一个位置移动到另一个位置(想想 RGB 颜色操作),请使用移位操作。无论哪种方式,您的源代码都会反映您实际在做什么。

                【讨论】:

                • 如果您真的对性能感兴趣而忽略源代码的清晰性怎么办。例如在哈希函数中......它对 .NET 或 Java 有帮助吗?
                • 这种微优化可能有助于特定 JIT 编译器。但是,您的代码将比运行它的 JIT 编译器的版本寿命更长,并且无法判断 JIT 编译器的下一个版本将执行哪些优化。甚至有可能您在前一个版本中为提高速度所做的某些事情在下一个版本中的性能更差!这种情况与编译步骤只执行一次的 C 语言非常不同。 JIT 编译的字节码语言可以在编写代码很长时间后提高性能。