【问题标题】:Java Enhanced-For-Loop FASTER than traditional?Java Enhanced-For-Loop 比传统更快?
【发布时间】:2014-01-03 04:13:53
【问题描述】:

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

对于少量迭代 (100-1000),增强的 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;
        }
    }
}

【问题讨论】:

标签: java performance loops for-loop


【解决方案1】:

基准测试错误。错误的非详尽列表:

  • 没有适当的热身:单次测量几乎总是错误的;
  • 在单个方法中混合多个代码路径:我们可能会开始使用仅适用于方法中第一个循环的执行数据来编译方法;
  • 来源是可预测的:如果循环编译,我们实际上可以预测结果;
  • 结果是死代码消除:如果循环编译,我们可以扔掉循环

花点时间听these talks,并通过these samples

这就是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 FORGOT TO POPULATE THE LIST,这意味着循环体永远不会执行,而基准实际上是衡量@的成本987654326@ 与 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),增强的 for 循环似乎在使用和不使用 JIT 的情况下要快得多。”

    我认为这种概括没有任何理由。我对程序做了一个小改动,首先运行基本的 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
    

    对于低迭代次数,结果非常依赖于程序的细节、微基准测试的内在危险,以及避免从单个程序观察推广到关于测量代码将如何运行的一般假设的原因在任何其他程序中执行。

    【讨论】:

    • 好建议。请记住,不仅存在 JITted 与非 JITted 的性能差异(这是允许大量预热时间的一个很好的理由),而且实际优化可能取决于使用代码的上下文(这使得即使在 JITted 之后,这种孤立的“微内核”测试也可能会产生误导)。
    • 还要记住,微优化通常是在浪费程序员的时间。对仅占程序总运行时间 1% 的事物进行无限优化可能会消耗无限的程序员时间,但最多只能产生 1% 的总性能提升。相反,应专注于高效算法并避免编写明显糟糕的代码,然后在运行时使用性能分析器找出程序真正在哪里花费时间并开始优化这些部分。跨度>
    猜你喜欢
    • 2016-05-15
    • 1970-01-01
    • 2014-05-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-01-12
    • 1970-01-01
    相关资源
    最近更新 更多