JAVA的加法乘法运行时间讨论

前言

在研究计算机底层的数字运算时书本告诉我们:乘法的实现往往靠的是加法的叠加。但是查阅某些资料时又会说CPU的ALU其实是内置了乘法器的但是底层实现好像也需要用到加法器,而且x86和x64的计算机也都提供MUL的汇编操作码,所以这些过于远古的事情真的是非常令人费解也令人着迷。
那么现在就本着一个JAVA程序员的角度去探索这个问题吧。

实验

执行1000000一百万次加法和乘法
888×8代码,三次结果是4939 5256 5025毫秒:

public class mullab {
public static void main(String[] args) {
    long startTime=System.currentTimeMillis();
    System.out.println("开始执行");
    int x=0;
    for(int i=0;i<1000000;i++) {
        x=888*8;
        System.out.println(x);
    }
    long endTime=System.currentTimeMillis();
    System.out.println("程序运行时间: "+(endTime - startTime)+"ms");
}
}

以下代码比较类似就不拿出来了。
8×888代码,三次结果是4927 4947 4947毫秒

888+888....(8次),三次结果是5367 5200 5128毫秒

8+8+8+8+8....(888次,三次结果是4954 5031 4973毫秒

8888×88888,三次结果是5367 5200 5128毫秒
可以见得他们的时间是在一个数量级的,这个问题就出在每次得到结果就进行一次打印输出占用了太多时间(IO时间是远远大于内存执行时间的)。

我以为如果不输出的话优化器就会把中间的for循环给优化成直接执行一次所以转变一下思路,加上System.gc(),强行销毁内存,最后发现gc占用时间也过多,对实验也会造成干扰。

既然for循环可能也有问题,那我就手动插入x+=3;x+=4;这样的代码一万行,人肉for循环,这该总行了吧,结果就是执行时间基本都是0ms,CPU这太强大了,眼看就要卡关了。

就要从JAVA运行的机制找问题了。

JAVA的执行机制

JAVA文件是.java文件是高级语言,计算机自然是看不懂的,所以应该使用javac -encoding utf-8 HelloWorld.java指令进行编译成字节码文件(.class文件也叫CAFEBABE文件),这时候JVM虚拟机就会加载字节码文件,然后使用内部的一些机制去调用计算机底层硬件执行你的程序。

编译与反编译

使用jd-gui反编译工具可以把字节码文件转成JAVA文件进行阅读。
我们发现:
1.之前疯狂插入的一万行x+=3;x+=4;代码是存在的。
2.x=8×88888;就会优化成 i = 711104;编译阶段会把常数进行运算,此语句直接变成赋值运算。
齐活,我们好像要抓住真理了。把x=8×88888改造成x=y×z不就行了吗?

再次实验

执行2147483647次加法和乘法(这次把数字改大点)
1000000×888代码,三次结果是3 3 3毫秒:

public class mullab {
public static void main(String[] args) {
    long startTime=System.currentTimeMillis();
    System.out.println("执行代码块/方法");
    int x=0;
    int y=1000000;
    int z=888;
    for(int i=0;i<2147483647;i++) {
    x=z*y;
    }
    System.out.println(x);
    long endTime=System.currentTimeMillis();
    System.out.println("程序运行时间: "+(endTime - startTime)+"ms");
}
}

x+=1000000×888代码,三次结果是54 53 54毫秒:

public class mullab {
public static void main(String[] args) {
    long startTime=System.currentTimeMillis();
    System.out.println("执行代码块/方法");
    int x=0;
    int y=1000000;
    int z=888;
    for(int i=0;i<2147483647;i++) {
    x+=z*y;
    }
    System.out.println(x);
    long endTime=System.currentTimeMillis();
    System.out.println("程序运行时间: "+(endTime - startTime)+"ms");
}
}

从这里可以看出来JVM还是会去缓存里找数的,for循环的执行不忠诚于程序员的想法。JAVA程序员真是一不小心就把程序写对了呢,想搞砸算法多不容易啊。但是难不倒我呀。
加法执行2147483647次。

int y=1000000;
for(int i=0;i<2147483647;i++) {
    x+=y;
}
执行时间 :56ms
int z=888;
for(int i=0;i<2147483647;i++) {
    x+=z;
}
执行时间 :58ms

乘法执行2147483647次。

int z=888;
for(int i=0;i<2147483647;i++) {
    x*=z;
}
执行时间:1880ms
int y=1000000;
for(int i=0;i<2147483647;i++) {
    x*=y;
}
执行时间: 1852ms

从以上可以看出,确实是乘法远远慢于加法,现在问题是乘法是不是真的是加法的叠加:

  int y=1000000;
  x=2147483647*y;
  执行时间: 1ms
  int y=888;
  x=2147483647*z;
  执行时间: 1ms

这说明用乘法比用加法叠加高效多了。

网上的说法

1.乘法比加法节省,因为对于cpu而言,有专门的 乘法器/移位器 运算单元。

2.x86系列中加法和乘法的执行周期数会和寻道址方式、数据类型等因素有关版系。当寻址方式中需要访问mem时,又会和Cache的命中情况有关。简单的说XX指令执行XX周期是不全准确的。

3.一个指令周期指的是cpu完成一个指令的时间(比如加法指令)cpu周期是指cpu完成一次微指令的时间(微指令就是具体驱动cpu哪些硬件工作的指令,详情请查)而一个微指令耗时1-4个时钟周期不等)当然有些操作如内存存储将耗时多的多,但是cache技术已经很好掩饰了这个问题。做实验的时候(8086)感觉2时钟周期的微指令比较多,而指令一般由2~8个微指令构成,也就是说,一般一个指令周期大概是8个时钟周期。题主说假设cpu主频2.5GHz,那么指令周期就是312.5MHz,再结合之前的论述,不难算出,完成一次乘法大概是6微秒。这还是按照8086这个古董货的技术标准,现代cpu会有更加完美的硬件布线和指令构成,再下降两三个微秒也不是不可能的

结论

尽管资料显示关于计算机底层加法和乘法的速度有许多争论,而且当今从事底层代码分析和硬件设计的人也比较少,资料的获取也比较有限。

但是通过今天的简单分析至少在JAVA编程里应该是奏效的,那就是:
1.乘法的执行时间确实没有加法来得快。
2.但是如果需要用到乘法仍然应当直接写乘法语句,乘法的实现一定不是单单做加法叠加那么简单应该还包含位移和逻辑判断操作。
3.乘法的乘数和被乘数大小位置不影响乘法运算速度。
4.同理有需要进行次方计算也要优先考虑使用MATH函数,JAVA给我们提供了一套比较完善的API体系,过多考虑底层算法是没必要的。
5.本次试验暂时还不能排除FOR循环跳转语句对运算速度的影响,但归根结底可以确认乘法性能非常棒。

因此真正要探讨计算机底层的加法乘法机制就要深入了解汇编语言和操作码以及CPU硬件图纸了。希望未来的某一天能够有机会接触这些东西吧。

附加题(除法)

使用相同的研究方法对除法进行试验研究,发现除法类似,只是除法的执行时间要比乘法还多一个数量级(10倍),具体就不展开了。

你可能感兴趣的:(JAVA的加法乘法运行时间讨论)