毁三观的 Java for 循环语句优化!

据说,这是一道谷歌的面试题。

就是有 3 个 for 循环,有的程序员会把 3 个一样的 for 循环合成一个来写,有的把一个 for 循环,扯成 3 个来写。比如下面的这道题。

//第一种情况
for(int i=0;i<100;i++){
    //opr 1
}
for(int i=0;i<100;i++){
    //opr 2
}
for(int i=0;i<100;i++){
    //opr 3
}
//第二种情况
for(int i=0;i<100;i++){
    //opr 1
    //opr 2
    //opr 3
}

那么从效率等方面来说,上述两种 Java for 循环有区别吗?

我相信大部分人都认同,第二种情况效率更高。但实际是这样吗?我们来实验一下!

重新认识 Java 中的 for 循环语句!

先说结论:单个多操作的循环性能和多个单操作循环差不多。大多数情况前者会好一点点。不过可以忽略。

现在我们来看我们的实验。分三种情况。

第一种,循环里直接执行简单操作。

public static long oneLoop(int times) {
    long start = System.nanoTime();
    for (int i = 0, x = 0; i < times; i++) {
        x++; x++; x++;
    }
    long end = System.nanoTime();
    return end - start;
}
public static long threeLoop(int times) {
    long start = System.nanoTime();
    for (int i = 0, x = 0; i < times; i++) {
        x++;
    }
    for (int i = 0, x = 0; i < times; i++) {
        x++;
    }
    for (int i = 0, x = 0; i < times; i++) {
        x++;
    }
    long end = System.nanoTime();
    return end - start;
}

第二种,循环里调用简单方法。

private static int plusOne(int i) {
    return ++i;
}
public static long oneLoopCallMethod(int times) {
    long start = System.nanoTime();
    for (int i = 0, x = 0; i < times; i++) {
        x = plusOne(x);
        x = plusOne(x);
        x = plusOne(x);
    }
    long end = System.nanoTime();
    return end - start;
}
public static long threeLoopCallMethod(int times) {
    long start = System.nanoTime();
    for (int i = 0, x = 0; i < times; i++) {
        x = plusOne(x);
    }
    for (int i = 0, x = 0; i < times; i++) {
        x = plusOne(x);
    }
    for (int i = 0, x = 0; i < times; i++) {
        x = plusOne(x);
    }
    long end = System.nanoTime();
    return end - start;
}

第三种,循环里模拟比较复杂的调用。

private static final Random R = new Random();
private static final char[] LETTERS = "abcdefghijklmnopqrstuvwxyz".toCharArray();
// 方法1
private static String randomWord() {
    int length = R.nextInt(10);
    char[] chars = new char[length];
    for (int i = 0; i < length; i++) {
        chars[i] = LETTERS[R.nextInt(LETTERS.length)];
    }
    return new String(chars);
}
// 方法2
private static String randomUpperWord() {
    return randomWord().toUpperCase();
}
// 方法3
private static int wordLength() {
    return randomWord().length();
}
// 测试
public static long oneLoopThreeOperation(int times) {
    long start = System.nanoTime();
    for (int i = 0; i < times; i++) {
        randomWord();
        randomUpperWord();
        wordLength();
    }
    long end = System.nanoTime();
    return end - start;
}
// 对应测试拆分成3个for循环
public static long threeLoopThreeOperation(int times) {
    long start = System.nanoTime();
    for (int i = 0; i < times; i++) {
        randomWord();
    }
    for (int i = 0; i < times; i++) {
        randomUpperWord();
    }
    for (int i = 0; i < times; i++) {
        wordLength();
    }
    long end = System.nanoTime();
    return end - start;
}

测试框架很简单,单元测试内循环 10000 次,测总时间(纳秒)。然后每个单元测试重复100次,丢弃前 10 次预热结果,计平均时间。

long[] result = new long[6];
int loops= 100;
for (int i = 0; i < loops; i++) {
    if (i >= 10) { // 丢弃前10次预热
        result[0] += oneLoop(10000);
        result[1] += threeLoop(10000);
        result[2] += oneLoopCallMethod(10000);
        result[3] += threeLoopCallMethod(10000);
        result[4] += oneLoopThreeOperation(10000);
        result[5] += threeLoopThreeOperation(10000);
    }
}
// 打印以上时间/90

结果有点出乎意料。

你真的会 for 循环吗?

  • 简单自增操作,单个for比三个for快一点。
  • 调用同一个方法,单个for比三个for慢了接近一倍。
  • 接下来复杂场景下的结果恢复正常,单个循环还是要好一些。

为了搞清楚为什么第二种方法为什么用三个for循环反而快,用 -XX:+PrintCompilation 参数打印了 TIJ 的编译情况。

真实的原因,因为用一个循环,只会内联 plusOne() 方法。

for (int i = 0, x = 0; i < times; i++) {
    x = plusOne(x);
    x = plusOne(x);
    x = plusOne(x);
}

但如果是相同的三次循环,TIJ 有可能直接把整个循环编译了。后面两次就会节省时间。

for (int i = 0, x = 0; i < times; i++) {
    x = plusOne(x);
}
for (int i = 0, x = 0; i < times; i++) { // TIJ编译了整个循环
    x = plusOne(x);
}
for (int i = 0, x = 0; i < times; i++) {
    x = plusOne(x);
}

有网友在 github 上,对这个测试改成 C++ 扩大到跑10亿(1E9)次计数。我想这个测试以后可能会被各个语言给玩坏!

对于这个结论,有 CPU 设计团队的人员回复到。针对于 for 的性能,cpu 使用的是 branch predictor 中的 loop 模块。这个模块会根据历史信息,去预测 for 循环未来的行为。

并从 3 个方面给出了分析:

  • 执行性能: 一个for循环比三个for循环,cpu在第一次训练过程中,会少出现两次misp,但是,在以后再次遇到这个for语句的预测过程中两种写法是一样的。总结:一个for循环会节省几十个时钟周期,具体数字和cpu架构和芯片频率有关,大约5ns左右。
  • 占用CPU资源:一个for循环会比三个for循环更少。这个资源只是预测中一个子模块的资源,并不是执行单元的资源。虽然,节省三分之一,但是,并不是很多。
  • Java代码可读性:代码的可读性和可维护性,对于软件工程师很重要的。以上三个for循环,所多占用的CPU资源和耽误的时间,对于整个程序来讲,实在微不足道。对于初学者,写出整洁的代码是十分重要的。

以上是不是出乎你们的意料!

毁三观的 Java for 循环语句优化!

: » 毁三观的 Java for 循环语句优化!

原创文章,作者:端木书台,如若转载,请注明出处:https://blog.ytso.com/252029.html

(0)
上一篇 2022年5月3日
下一篇 2022年5月3日

相关推荐

发表回复

登录后才能评论