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

时间:2018-02-23 16:40:27

标签: java performance optimization 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);
    }

正在衡量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);
    }

以下是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 + '}';
    }
}

caps是一个ArrayList<Capsule>,其中有2000万个元素在开头填充:

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);
        }
    }

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

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

  • MultipleLoops:4.9661秒
  • SingleLoop:7.2725秒

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

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

更新1:

正如评论中所建议的,如果我在生成字符串时更改实现以计算totalLength,从而避免创建res列表,则单循环版本会变得更快。

但是,只引入了该变量,以便在创建结果列表后完成一些工作,以避免在没有使用它们的情况下丢弃元素。

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

结果:

  • MultipleLoops:0.9339秒
  • SingleLoop:0.66590005秒

更新2:

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

结果:

  • MultipleLoops:7.397秒
  • SingleLoop:8.092秒

2 个答案:

答案 0 :(得分:2)

我调查了这个“现象”,看起来像是一个回答 我们将.jvmArgs("-verbose:gc")添加到JMH OptionsBuilder。 1迭代的结果:

  

单环:[全GC(人体工程学)[PSYoungGen:2097664K-> 0K(2446848K)] [ParOldGen:3899819K-> 4574771K(5592576K)] 5997483K-> 4574771K(8039424K),[Metaspace:6208K- &gt; 6208K(1056768K)],5.0438301秒] [时间:用户= 37.92 sys = 0.10,实际= 5.05秒]   4.954 s / op

     

多重循环:[全GC(人机工程学)[PSYoungGen:2097664K-> 0K(2446848K)] [ParOldGen:3899819K-> 4490913K(5592576K)] 5997483K-> 4490913K(8039424K),[Metaspace:6208K- &gt; 6208K(1056768K)],3.7991573秒] [时间:用户= 26.84 sys = 0.08,实际= 3.80秒]   4.187 s / op

JVM为GC花费了大量的CPU时间。每运行2次测试,JVM必须制作Full GC(将600Mb移至OldGen并从之前的周期收集1.5Gb的垃圾)。两个垃圾收集器完成了相同的工作,但是多个循环测试用例的应用时间缩短了约25%。如果我们将POPSIZE减少到10_000_000或者添加到bh.consume() Thread.sleep(3000)之前,或者将-XX:+UseG1GC添加到JVM args,那么多循环增强效果就会消失。我用.addProfiler(GCProfiler.class)再次运行它。主要区别:

  

多圈:gc.churn.PS_Eden_Space 374.417±23 MB /秒

     

单循环:gc.churn.PS_Eden_Space 336.037 MB /秒±19 MB /秒

我认为,我们看到在这种特定情况下加速,因为旧的比较和交换GC算法在多次测试运行中具有CPU瓶颈,并且对于早期运行中的收集垃圾使用额外的“无意义”循环。如果你有足够的RAM,使用@Threads(2)重现更容易。如果您尝试配置Single_Loop测试,它看起来像这样:

profiling

答案 1 :(得分:1)

要了解幕后发生的情况,您可以添加JMX行为来分析位于 JAVA_HOME \ bin jvisualvm 中正在运行的应用程序 内存中有20M大小的封装列表,内存耗尽,visualvm进入无响应状态。如果要测试的话,我已经将胶囊列表大小减小到200k和100M到1M。在观察visualvm上的行为之后,在多个循环之前完成单循环执行。也许这不是正确的方法,但你可以尝试一下。

<强> LoopBean.java

import java.util.List;
public interface LoopMBean {
    void multipleLoops();
    void singleLoop();
    void printResourcesStats();
}

<强> Loop.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());
    }
}

<强> 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();
        }
    }
}

使用以下命令执行:

java -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9999 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false LoopClient

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