为什么两个独立的循环比一个快?

Fra*_*iro 23 java optimization performance benchmarking microbenchmark

我想了解Java对连续循环做了什么样的优化.更确切地说,我正在尝试检查是否执行了循环融合.从理论上讲,我期待这种优化不会自动完成,并且期望确认融合版本比具有两个循环的版本更快.

但是,在运行基准测试之后,结果显示两个独立(和连续)循环比完成所有工作的单个循环更快.

我已经尝试使用JMH创建基准测试并获得相同的结果.

我使用了该javap命令,它显示生成的具有两个循环的源文件的字节码实际上对应于正在执行的两个循环(没有循环展开或执行其他优化).

正在测量的代码BenchmarkMultipleLoops.java:

private void work() {
        List<Capsule> intermediate = new ArrayList<>();
        List<String> res = new ArrayList<>();
        int totalLength = 0;

        for (Capsule c : caps) {
            if(c.getNumber() > 100000000){
                intermediate.add(c);
            }
        }

        for (Capsule c : intermediate) {
            String s = "new_word" + c.getNumber();
            res.add(s);
        }

        //Loop to assure the end result (res) is used for something
        for(String s : res){
            totalLength += s.length();
        }

        System.out.println(totalLength);
    }
Run Code Online (Sandbox Code Playgroud)

正在测量的代码BenchmarkSingleLoop.java:

private void work(){
        List<String> res = new ArrayList<>();
        int totalLength = 0;

        for (Capsule c : caps) {
            if(c.getNumber() > 100000000){
                String s = "new_word" + c.getNumber();
                res.add(s);
            }
        }

        //Loop to assure the end result (res) is used for something
        for(String s : res){
            totalLength += s.length();
        }

        System.out.println(totalLength);
    }
Run Code Online (Sandbox Code Playgroud)

以下是代码Capsule.java:

public class Capsule {
    private int number;
    private String word;

    public Capsule(int number, String word) {
        this.number = number;
        this.word = word;
    }

    public int getNumber() {
        return number;
    }

    @Override
    public String toString() {
        return "{" + number +
                ", " + word + '}';
    }
}
Run Code Online (Sandbox Code Playgroud)

caps是的ArrayList<Capsule>,在开始填充像这20个百万个元素:

private void populate() {
        Random r = new Random(3);

        for(int n = 0; n < POPSIZE; n++){
            int randomN = r.nextInt();
            Capsule c = new Capsule(randomN, "word" + randomN);
            caps.add(c);
        }
    }
Run Code Online (Sandbox Code Playgroud)

在测量之前,执行预热阶段.

我运行了每个基准测试10次,换句话说,work()每个基准测试执行10次方法,完成的平均时间如下(以秒为单位).每次迭代后,GC都会执行几次睡眠:

  • MultipleLoops:4.9661秒
  • SingleLoop:7.2725秒

OpenJDK 1.8.0_144在Intel i7-7500U(Kaby Lake)上运行.

为什么MultipleLoops版本比SingleLoop版本更快,即使它必须遍历两个不同的数据结构?

更新1:

正如评论中所建议的,如果我改变实现来计算totalLengthwhile字符串的生成,避免创建res列表,单循环版本会变得更快.

但是,只引入了该变量,以便在创建结果列表后完成一些工作,以避免在没有对它们进行任何操作时丢弃这些元素.

换句话说,预期的结果是产生最终列表.但这个建议有助于更好地了解正在发生的事情.

结果:

  • MultipleLoops:0.9339秒
  • SingleLoop:0.66590005秒

更新2:

这是我用于JMH基准的代码的链接:https: //gist.github.com/FranciscoRibeiro/2d3928761f76e4f7cecfcfcdf7fc96d5

结果:

  • MultipleLoops:7.397秒
  • SingleLoop:8.092秒

小智 1

要了解幕后发生的情况,您可以添加 JMX 行为来分析jvisualvm中正在运行的应用程序,位于JAVA_HOME\bin 内存中有 20M 大小的胶囊列表,它耗尽了内存,并且 Visualvm 进入无响应状态。我在 if 条件下将胶囊列表大小减少到 200k,将 100M 减少到 1M 进行测试。观察 VisualVM 上的行为后,单循环执行先于多个循环执行。也许这不是正确的方法,但您可以尝试一下。

LoopBean.java

import java.util.List;
public interface LoopMBean {
    void multipleLoops();
    void singleLoop();
    void printResourcesStats();
}
Run Code Online (Sandbox Code Playgroud)

循环.java

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class Loop implements LoopMBean {

    private final List<Capsule> capsules = new ArrayList<>();

    {
        Random r = new Random(3);
        for (int n = 0; n < 20000000; n++) {
            int randomN = r.nextInt();
            capsules.add(new Capsule(randomN, "word" + randomN));
        }
    }

    @Override
    public void multipleLoops() {

        System.out.println("----------------------Before multiple loops execution---------------------------");
        printResourcesStats();

        final List<Capsule> intermediate = new ArrayList<>();
        final List<String> res = new ArrayList<>();
        int totalLength = 0;

        final long start = System.currentTimeMillis();

        for (Capsule c : capsules)
            if (c.getNumber() > 100000000) {
                intermediate.add(c);
            }

        for (Capsule c : intermediate) {
            String s = "new_word" + c.getNumber();
            res.add(s);
        }

        for (String s : res)
            totalLength += s.length();

        System.out.println("multiple loops=" + totalLength + " | time taken=" + (System.currentTimeMillis() - start) + " milliseconds");

        System.out.println("----------------------After multiple loops execution---------------------------");
        printResourcesStats();

        res.clear();
    }

    @Override
    public void singleLoop() {

        System.out.println("----------------------Before single loop execution---------------------------");
        printResourcesStats();

        final List<String> res = new ArrayList<>();
        int totalLength = 0;

        final long start = System.currentTimeMillis();

        for (Capsule c : capsules)
            if (c.getNumber() > 100000000) {
                String s = "new_word" + c.getNumber();
                res.add(s);
            }

        for (String s : res)
            totalLength += s.length();

        System.out.println("Single loop=" + totalLength + " | time taken=" + (System.currentTimeMillis() - start) + " milliseconds");
        System.out.println("----------------------After single loop execution---------------------------");
        printResourcesStats();

        res.clear();
    }

    @Override
    public void printResourcesStats() {
        System.out.println("Max Memory= " + Runtime.getRuntime().maxMemory());
        System.out.println("Available Processors= " + Runtime.getRuntime().availableProcessors());
        System.out.println("Total Memory= " + Runtime.getRuntime().totalMemory());
        System.out.println("Free Memory= " + Runtime.getRuntime().freeMemory());
    }
}
Run Code Online (Sandbox Code Playgroud)

LoopClient.java

import javax.management.MBeanServer;
import javax.management.ObjectName;
import java.lang.management.ManagementFactory;

public class LoopClient {

    void init() {

        final MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
        try {
            mBeanServer.registerMBean(new Loop(), new ObjectName("LOOP:name=LoopBean"));
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    public static void main(String[] args) {

        final LoopClient client = new LoopClient();
        client.init();
        System.out.println("Loop client is running...");
        waitForEnterPressed();
    }

    private static void waitForEnterPressed() {
        try {
            System.out.println("Press  to continue...");
            System.in.read();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

使用以下命令执行:

java -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9999 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false LoopClient
Run Code Online (Sandbox Code Playgroud)

您可以添加-Xmx3072M额外选项以快速增加内存以避免 OutOfMemoryError