You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
android-notes/blogs/JVM/循环优化.md

5.5 KiB

循环优化

目录

  1. 概述
  2. 循环无关代码外提
  3. 循环展开
  4. 循环判断外提
  5. 循环剥离
  6. 参考

概述

在许多应用程序中,循环都扮演着非常重要的角色。为了提升循环的运行效率,研发编译器的工程师提出了不少面向循环的编译优化方式:

  1. 循环无关代码外提
  2. 循环展开
  3. 循环判断外提
  4. 循环剥离

循环无关代码外提

所谓的循环无关代码,指的是循环中值不变的表达式。如果能够在不改变程序语义的情况下,将这些循环无关代码提出循环之外,那么程序便可以避免重复执行这些表达式,从而达到性能提升的效果。

    private void calc(int x, int y, int[] array) {
        int sum = 0;
        for (int i = 0; i < array.length; i++) {
            sum += x * y + array[i];
        }
        System.out.println(sum);
    }

理想情况下,上面这段代码经过无关代码外提之后,等同于以下代码:

    private void calc(int x, int y, int[] array) {
        int sum = 0;
        int length = array.length;
        int t = x * y;
        for (int i = 0; i < length; i++) {
            sum += t + array[i];
        }
        System.out.println(sum);
    }

循环展开

另外一项非常重要的循环优化是循环展开,它指的是在循环体中重复多次循环迭代,并减少循环次数的编译优化。

    private void calc(int[] array) {
        int sum = 0;
        for (int i = 0; i < array.length; i++) {
            sum += (i % 2 == 0) ? array[i] : -array[i];
        }
        System.out.println(sum);
    }

经过循环展开之后将形成下面的代码:

    private void calc(int[] array) {
        int sum = 0;
        for (int i = 0; i < array.length; i += 2) {
            sum += (i % 2 == 0) ? array[i] : -array[i];
            sum += ((i + 1) % 2 == 0) ? array[i + 1] : -array[i + 1];
        }
        System.out.println(sum);
    }

不过循环展开的缺点也是显而易见:它可能增加了代码的冗余度,导致所生成的机器码的长度大幅上涨。

不过随着循环体的增大,优化机会也会不断增加。一旦循环展开能够触发进一步的优化,总体的代码复杂度也将降低。所以以上代码可以进一步优化为:

    private void calc(int[] array) {
        int sum = 0;
        for (int i = 0; i < array.length; i += 2) {
            sum += array[i];
            sum += -array[i + 1];
        }
        System.out.println(sum);
    }

循环展开有一种特殊情况,那便是完全展开。当循环的数目是固定值而且非常小时,即使编译器会将循环全部展开。

    private void calc(int[] array) {
        int sum = 0;
        for (int i = 0; i < 4; i++) {
            sum += array[i];
        }
        System.out.println(sum);
    }

以上代码会将完全展开为下述代码:

    private void calc(int[] array) {
        int sum = 0;
        sum += array[0];
        sum += array[1];
        sum += array[2];
        sum += array[3];
        System.out.println(sum);
    }

即时编译器会在循环体的大小与循环展开次数之间做出权衡。

循环判断外提

循环判断外提是指将循环中的 if 语句外提至循环之前,并且在该 if 语句的两个分支中分别放置一份循环代码。

    private void calc(int[] array) {
        int sum = 0;
        for (int i = 0; i < array.length; i++) {
            if (array.length > 4) {
                sum += array[i];
            }
        }
        System.out.println(sum);
    }

以上代码经过循环判断外提之后,将变成下面代码:

    private void calc(int[] array) {
        int sum = 0;
        if (array.length > 4) {
            for (int i = 0; i < array.length; i++) {
                sum += array[i];
            }
        } else {
            for (int i = 0; i < array.length; i++) {
                
            }
        }
        System.out.println(sum);
    }

再经过进一步优化变成了:

    private void calc(int[] array) {
        int sum = 0;
        if (array.length > 4) {
            for (int i = 0; i < array.length; i++) {
                sum += array[i];
            }
        }
        System.out.println(sum);
    }

循环剥离

循环剥离指的是将循环的前几个迭代或者后几个迭代剥离出循环的优化方式。一般来说,循环的前几个迭代或者后几个迭代都包含特殊处理,通过将这几个特殊的迭代剥离出去,可以使原本的循环体的规律性更加明显,从而触发进一步的优化。

private void calc(int[] array) {
    int sum = 0;
    int j = 0;
    for (int i = 0; i < array.length; i++) {
        sum += array[j];
        j = i;
    }
    System.out.println(sum);
}

经过循环剥离之后,代码可能变成以下:

    private void calc(int[] array) {
        int sum = 0;
        if (array.length > 0) {
            sum += array[0];
            for (int i = 1; i < array.length; i++) {
                sum += array[i - 1];
            }
        }
        System.out.println(sum);
    }

事实上,可能并不会有人这样写代码。可以看出,循环剥离就是在不改变程序语义的情况下尽可能的去除掉无关代码,少生成无关变量。

参考

循环优化