Java增强for循环比传统的for循环更快吗?

3

我的理解是增强型for循环应该更慢,因为它们必须使用迭代器。但是我的代码提供了不同的结果。(是的,我知道循环逻辑占用了循环大部分时间)

对于较少的迭代次数(100-1000),增强型for循环似乎比传统for循环快得多,无论有没有启用JIT编译。相反,当迭代次数很高(100000000)时,传统的循环要快得多。到底发生了什么?

public class NewMain {

    public static void main(String[] args) {

        System.out.println("Warming up");

        int warmup = 1000000;
        for (int i = 0; i < warmup; i++) {
            runForLoop();
        }
        for (int i = 0; i < warmup; i++) {
            runEnhancedFor();
        }

        System.out.println("Running");
        int iterations = 100000000;
        long start = System.nanoTime();
        for (int i = 0; i < iterations; i++) {
            runForLoop();
        }
        System.out.println((System.nanoTime() - start) / iterations + "nS");

        start = System.nanoTime();
        for (int i = 0; i < iterations; i++) {
            runEnhancedFor();
        }
        System.out.println((System.nanoTime() - start) / iterations + "nS");
    }

    public static final List<Integer> array = new ArrayList(100);

    public static int l;

    public static void runForLoop() {
        for (int i = 0; i < array.size(); i++) {
            l += array.get(i);
        }
    }

    public static void runEnhancedFor() {
        for (int i : array) {
            l += i;
        }
    }
}

2
不要使用System.nanoTime(),它不可靠。 - Elliott Frisch
2
这个目的非常可靠。你有什么建议? - Colby
似乎除非您执行数百万次循环,否则增强循环速度更快。 - Colby
@Colby 你正在生成随机数。阅读这篇文章 - Elliott Frisch
显示剩余2条评论
2个回答

37

基准测试存在问题。以下是不完全的错误列表:

  • 没有适当的预热:单次测量几乎总是错误的;
  • 在单个方法中混合多个代码路径:我们可能会从方法中仅获得第一个循环的执行数据开始编译该方法;
  • 源是可预测的:如果循环被编译,实际上我们可以预测结果;
  • 结果已消除死代码:如果循环被编译,我们可以放弃这个循环。

花时间收听这些演讲,并浏览这些示例

以下是使用jmh正确参考的方式:

@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 3, time = 1)
@Fork(3)
@State(Scope.Thread)
public class EnhancedFor {

    private static final int SIZE = 100;

    private List<Integer> list;

    @Setup
    public void setup() {
        list = new ArrayList<Integer>(SIZE);
    }


    @GenerateMicroBenchmark
    public int enhanced() {
        int s = 0;
        for (int i : list) {
            s += i;
        }
        return s;
    }

    @GenerateMicroBenchmark
    public int indexed() {
        int s = 0;
        for (int i = 0; i < list.size(); i++) {
            s += list.get(i);
        }
        return s;
    }

    @GenerateMicroBenchmark
    public void enhanced_indi(BlackHole bh) {
        for (int i : list) {
            bh.consume(i);
        }
    }

    @GenerateMicroBenchmark
    public void indexed_indi(BlackHole bh) {
        for (int i = 0; i < list.size(); i++) {
            bh.consume(list.get(i));
        }
    }

}

...这会得到类似以下的结果:

Benchmark                         Mode   Samples      Mean   Mean error    Units
o.s.EnhancedFor.enhanced          avgt         9     8.162        0.057    ns/op
o.s.EnhancedFor.enhanced_indi     avgt         9     7.600        0.067    ns/op
o.s.EnhancedFor.indexed           avgt         9     2.226        0.091    ns/op
o.s.EnhancedFor.indexed_indi      avgt         9     2.116        0.064    ns/op

现在增强循环和索引循环之间存在微小差异,这个差异通常被解释为访问后备存储的不同代码路径。然而,实际上这个解释更简单:OP忘记了填充列表,这意味着循环体永远不会被执行,而该基准实际上衡量的是size()iterator()的成本差异!

修复方法:

@Setup
public void setup() {
    list = new ArrayList<Integer>(SIZE);
    for (int c = 0; c < SIZE; c++) {
        list.add(c);
    }
}

则产生:

Benchmark                         Mode   Samples       Mean   Mean error    Units
o.s.EnhancedFor.enhanced          avgt         9    171.154       25.892    ns/op
o.s.EnhancedFor.enhanced_indi     avgt         9    384.192        6.856    ns/op
o.s.EnhancedFor.indexed           avgt         9    148.679        1.357    ns/op
o.s.EnhancedFor.indexed_indi      avgt         9    465.684        0.860    ns/op

请注意,即使在纳米尺度上,差异也非常微小,并且如果有任何差异,非平凡的循环体将消耗差异。这里的差异可以通过我们运用 get()Iterator方法的幸运程度以及在进行这些内联后享受的优化来解释。

请注意 indi_* 测试,它们否定了循环展开优化。尽管在成功展开时,indexed 享有更好的性能,但当展开被打破时情况则相反!

像这样的标题,indexedenhanced 之间的差异只是学术兴趣而已。找出所有情况的确切生成代码 -XX:+PrintAssembly 留给读者作为练习 :)


2

这个问题涉及到两个非常不同的方面。其一是一个合理的观察,即在一个特定的程序中,当迭代次数较低时,增强型for循环的时间更快。另一个是将该观察推广到“对于低迭代次数(100-1000),无论是否启用JIT,增强型for循环似乎都要快得多”的概括性结论。

我认为这种概括没有任何理由。我对程序进行了小改动,先运行基本的for循环测试,然后再运行增强型for循环。我还标记了输出以减少处理修改版本时的混淆。以下是我针对100次迭代的输出:

Warming up
Running
Enhanced For-Loop 2002nS
Basic For-Loop 70nS

使用原始顺序循环,我得到:

Warming up
Running
Basic For-Loop 2139nS
Enhanced For-Loop 137nS

如果我在运行第二个循环之前立即对其进行预热,而不是在开始时同时进行预热,则会获得以下结果:
Warming up
Running
Basic For-Loop 1093nS
Enhanced For-Loop 984nS

低迭代次数的结果非常依赖于程序的细节,这是微基准测试的固有危险,并且是避免从单个程序观察推广到关于测量代码在任何其他程序中执行方式的一般假设的原因。

好的建议。记住,不仅有JIT与非JIT性能差异(这也是允许足够的预热时间的一个很好的理由),而且实际优化可能取决于代码的使用上下文(这使得单独的“微内核”测试在JIT之后潜在地具有误导性)。 - keshlam
1
请记住,微小的优化常常会浪费程序员的时间。 无限地优化仅占程序总运行时间1%的内容可能需要消耗无限的程序员工作时间,但最多只能获得1%的总性能提升。 相反,应该专注于高效算法并避免编写明显错误的代码,然后在程序运行时使用性能分析器来查找程序实际所花费的时间,并开始优化这些部分。 - keshlam

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接