在Java中移位位是否比乘法和除法快?。净?[关闭]


68

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


17
为什么要问我们?尝试一下,您就会知道!两种方式编写代码,拿出秒表,然后看看会发生什么。
埃里克·利珀特

12
发行新版本的jvm或clr后,答案将无效。或当有人在嵌入式系统上尝试时。或移动CPU。或GPU。或当满月。或当行星对齐时。或者...(您知道了。)
Greg D

3
@Eric为什么需要做更多的工作?花费1分钟时间问关于SO的问题,去吃三明治,然后回来找到答案,详细说明原因。
Rex M

2
我们的观点将比实验中的观点更快地过时。IE已经过时或与下一个cpu vm等配合使用。因此,只需编写尽可能清晰的代码,以使优化器可以理解它,从而对其进行优化。
ctrl-alt-delor 2012年

2
尽管下面的答案谁都发誓现代编译器太聪明了,以至于不能错过这种事情,但是我在C#4.0上的测试表明,对于an int i,该语句(i << 3)(i * 8)Release模式下的语句快15%到25%。这在各种条件执行流中都是一致的,因此它似乎不是最优化。绝对应该在性能至关重要的循环中对自己进行测试。JVM比.NET CLR更加优化,因此,如果这种特殊的低效率仅适用于.NET,这也不会令我感到惊讶。
特制调味酱

Answers:


90

如今,大多数编译器所做的不只是将乘或除以2的幂转换为移位运算。优化时,即使不是2的幂,许多编译器也可以使用编译时间常数来优化乘法或除法。通常,乘法或除法可以分解为一系列移位和加法,并且如果该一系列操作会更快比乘或除,编译器将使用它。

对于常数除法,编译器通常可以将运算转换为乘以“魔数”后跟移位的乘数。由于乘法通常比除法运算快得多,因此这可以节省大量的时钟周期。

亨利·沃伦(Henry Warren)的书《骇客的喜悦》(Hacker's Delight)中有许多与此主题相关的信息,在相应的网站上也有很好的介绍:

另请参阅以下内容中的讨论(带有一个或两个链接):

无论如何,所有这些归结为使编译器可以处理微优化的繁琐细节。自从执行自己的工作已经超过编译器已有好几年了。


6
您应该注意,如果x是一个奇数负数x/2x>>1它将计算不同的事物。如果有人在乎一个奇数负数的计算将如何处理,则应使用产生正确答案的形式。
2013年

92

几乎所有值得盐溶的环境都会为您优化这一点。如果没有,则您需要炸更大的鱼。认真地,不要再浪费时间对此进行思考。您将在遇到性能问题时知道。在运行了探查器之后,您将知道造成它的原因,并且应该很清楚如何解决它。

您将永远不会听到有人说“我的应用程序速度太慢,然后我开始随机替换为x * 2x << 1并且一切都已修复!” 解决性能问题的方法通常是找到一种减少工作量级的方法,而不是找到一种以1%的速度完成相同工作的方法。


16
+1代表“在您运行探查器之后...”
格兰特·瓦格纳

6
使用更清晰的那个。如果它是乘法或除法的常数,则编译器几乎肯定会做正确的事情。
杰森·克赖顿

4
真正的好答案-过早的优化几乎总是在浪费时间。
大卫·蒂伦

4
如果x可能是奇数负数,则x>>1和的行为x/2是不同的。除非两个操作都正确(例如,因为x永远不会是奇数负数,否则其他地方的代码将处理一对一的条件),否则更快的问题将是无关紧要的。与反之亦然,我更经常看到x/2什么时候x>>=1才是正确的[寻找程序,其图形在原点X和Y坐标处有难看的接缝]。
2013年

通常,当人们在答案中添加自己的观点时,我不会感到高兴,但这只是纯粹的事实。也许在90年代,位移是一个值得进行的性能优化,但是现在将位移作为具有现代编译器复杂性和功能的“性能突破”是愚蠢的。
Kaiser Keister

33

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

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

编程时要准确地描述您想完成的事情,而不是如何去做。可以分别独立地改进和优化JIT,VM,编译器和CPU的未来版本。如果您指定的内容如此微小和具体,则会失去所有未来优化的好处。


53
...以及83.7%的统计数字构成了:-)
Stephen C

2
+1考虑未来
Trampas Kirk

20

您几乎可以肯定地将2的幂乘积优化依赖于移位运算。这是编译器构造学生将要学习的第一个优化方法之一。:)

但是,我认为没有任何保证。您的源代码应该反映您的意图,而不是试图告诉优化器该怎么做。如果您要增加数量,请使用乘法。如果要将位字段从一个位置移到另一个位置(请考虑RGB颜色操作),请使用移位操作。无论哪种方式,您的源代码都将反映您的实际工作。


如果您真的对性能有兴趣而不考虑源代码的清晰度,该怎么办。例如,在散列函数中...它对.NET或Java有帮助吗?
豪尔赫·科尔多瓦

2
这种微优化可能会帮助特定的JIT编译器。但是,您的代码将比运行它的JIT编译器的版本寿命更长,并且无法确定下一个JIT编译器版本将执行哪些优化。甚至可能您在上一个版本中为了使其更快而所做的某些事情在下一个版本中的性能更差!这种情况与C之类的语言非常不同,后者仅执行一次编译步骤。编写代码后很久,JIT编译的字节码语言可以提高性能。
格雷格·休吉尔

13

请注意,对于负数,奇数,向下和除法运算(肯定会在Java中)会得出不同的结果。

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

印刷品:

Shift: -4
Div:   -3

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


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;

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


5

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


3

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

在现代(Pentium Pro及更高版本)硬件上,流水线化使这一切变得无关紧要,而且人迹罕至的做法通常意味着您失去了很多无法获得的优化。

微观优化不仅浪费您的时间,而且很难正确实现。


1

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


1

根据微基准测试的结果,移动速度是除法运算(Oracle Java 1.7.0_72)的两倍。


0

在适当的时候,大多数编译器会将乘法和除法转换为位移。这是最简单的优化之一。因此,对于给定的任务,您应该执行更容易阅读且更适合的工作。


0

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

(编辑:更改了代码,以在迈克尔·迈尔斯的建议后停止溢出,但是结果是相同的!这是什么问题?)

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毫秒


1
您可以对其进行基准测试,使其不会溢出吗?如果我没记错的话,溢出可能会很昂贵。
迈克尔·迈尔斯

2
对于未专门为其准备的机器上的单个基准,这实际上是相同的。
Joel Coehoorn

2
您是否尝试过多次运行以查看结果是否保留?
luiscubal

1
续:实际的<<或*将与for循环j ++ / branch代码并行执行,这可能会占主导地位。
汉克·霍尔特曼

2
您正在为乘法案例初始化新变量,而对于位移位案例则重新使用日期变量。那里可能有GC的簿记。另外,如果使用“ << =”而不是“ <<”,则位移可能会更好。这将使移位大小写的情况与用于乘法大小写的“ * =”更相似。现在,您可能会认为编译器会对此进行优化,但是我想这取决于编译器。乘法可能已针对位移位进行了优化,但显式分配可能尚未针对位置移位进行了优化。
卡尔
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.