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

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

但是,在运行基准测试后,结果表明两个单独的(和连续的)循环比一个完成所有工作的单个循环更快。

我已经尝试使用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是一个包含 2000 万个元素的,一开始是这样填充的:ArrayList<Capsule>

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次,换句话说,该方法为每个基准测试执行10次,完成的平均时间如下所示(以秒为单位)。每次迭代后,都会执行 GC 以及一些休眠:work()

  • 多回路:4.9661 秒
  • 单回点:7.2725秒

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

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

更新 1:

正如注释中建议的那样,如果我更改实现以计算生成字符串的时间,从而避免创建列表,则单循环版本会变得更快。totalLengthres

但是,引入该变量只是为了在创建结果列表后完成一些工作,以避免在未对元素执行任何操作时丢弃这些元素。

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

结果:

  • 多回点:0.9339 秒
  • 单回点:0.66590005秒

更新 2:

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

结果:

  • 多回点:7.397 秒
  • 单循环:8.092秒

答案 1

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

单循环: [全GC (人体工程学) [PSYoungGen: 2097664K->0K(2446848K)] [ParOldGen: 3899819K->4574771K(5592576K)] 5997483K->4574771K(8039424K), [元空间: 6208K->6208K(1056768K)], 5.0438301 秒] [次数: user=37.92 sys=0.10, real=5.05 secs] 4.954 s/op

多循环: [完整GC (人体工程学) [PSYoungGen: 2097664K->0K(2446848K)] [ParOldGen: 3899819K->4490913K(5592576K)] 5997483K->4490913K(8039424K), [元空间: 6208K->6208K(1056768K)], 3.7991573 秒] [次数: user=26.84 sys=0.08, real=3.80 secs] 4.187 s/op

JVM 为 GC 花费了大量的 CPU 时间。每运行2次测试,JVM必须制作完整的GC(将600Mb移动到OldGen并从以前的循环中收集1.5Gb的垃圾)。两个垃圾回收器都做了相同的工作,但为多个循环测试用例花费了大约25%的应用程序时间。如果我们将 减小到 10_000_000 或添加到 之前,或添加到 JVM 参数,则多循环提升效果就消失了。我用.主要区别:POPSIZEbh.consume()Thread.sleep(3000)-XX:+UseG1GC.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,则使用 甚至更容易重现。它看起来像这样,如果你尝试分析Single_Loop测试:@Threads(2)

profiling


答案 2

要了解引擎盖下发生了什么,您可以添加 JMX 行为来分析 jvisualvm 中正在运行的应用程序,jvisualvm 位于 JAVA_HOME\bin 内存中胶囊列表的大小为 20M,内存不足,visualvm 处于无响应状态。在测试条件下,我将胶囊列表大小减少到200k和100M减少到1M。在 visualvm 上观察行为后,在多个循环之前完成单循环执行。也许这不是正确的方法,但你可以尝试一下。

LoopBean.java

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

循环.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 额外选项以快速增加内存,以避免内存不足错误


推荐