提取方法重构后,代码变慢了6倍

我知道微基准测试很难。我并不是要建立一个糟糕的微型基准。相反,在进行(我认为是)无害重构时,我遇到了这个问题。下面是该问题的简化演示。

程序将构建一个包含一万个随机整数的ArrayList,然后查找元素的总和。在该示例中,将求和重复一百万次,以改善经过时间的测量中的信号与噪声比。在实际程序中,有一百万个略有不同的列表,但是不管怎样,问题的效果仍然成立。

  • App#arraySumInlined是重构之前重构的方法版本,其求和保持在循环正文中。
  • App#arraySumSubFunctionCall是将循环体提取到单独方法中的方法版本。

现在,令我惊讶的是,arraySumInlined花费了约7秒,而arraySumSubFunctionCall花费了约42秒。在我看来,这似乎是一个令人印象深刻的差异。

如果我取消对arraySumInlinedarraySumSubFunctionCall的评论,则它们将分别在约7秒内完成。即arraySumSubFunctionCall不再那么慢。

这是怎么回事?有更广泛的含义吗?例如。我从来没有想过将提取方法重构看作可以将7秒方法调用变成42秒方法的事情。

在研究此问题时,我发现了几个涉及JIT的问题(例如Java method call performanceWhy does this code using streams run so much faster in Java 9 than Java 8?),但它们似乎处理相反的情况:内联代码执行 worse 比代码在单独的方法中。

环境详细信息:Windows 10 x64,Intel Core i3-6100。

λ java -version
openjdk version "11.0.4" 2019-07-16
OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.4+11)
OpenJDK 64-Bit Server VM AdoptOpenJDK (build 11.0.4+11,mixed mode)

λ javac -version
javac 11.0.4
import java.util.ArrayList;
import java.util.Random;
import java.util.concurrent.TimeUnit;

public class App {

  public static void main(String[] args) {
    final int size = 10_000;
    final int iterations = 1_000_000;
    final var data = integerListWithRandomValues(size);

    //arraySumInlined(iterations,data);
    arraySumSubFunctionCall(iterations,data);
  }

  private static void arraySumSubFunctionCall(int iterations,final ArrayList<Integer> data) {
    final long start = System.nanoTime();
    long result = 0;
    for (int i = 0; i < iterations; ++i) {
      result = getSum(data);
    }
    final long end = System.nanoTime();
    System.out.println(String.format("%f sec (%d)",TimeUnit.NANOSECONDS.toMillis(end - start) / 1000.0,result));
  }

  private static void arraySumInlined(int iterations,final ArrayList<Integer> data) {
    final long start = System.nanoTime();
    long result = 0;
    for (int i = 0; i < iterations; ++i) {
      result = data.stream().mapToInt(e -> e).sum();
    }
    final long end = System.nanoTime();
    System.out.println(String.format("%f sec (%d)",result));
  }

  private static int getSum(final ArrayList<Integer> data) {
    return data.stream().mapToInt(e -> e).sum();
  }

  private static ArrayList<Integer> integerListWithRandomValues(final int size) {
    final var result = new ArrayList<Integer>();
    final var r = new Random();

    for (int i = 0; i < size; ++i) {
      result.add(r.nextInt());
    }

    return result;
  }
}
zxcvbnmasd1234 回答:提取方法重构后,代码变慢了6倍

我对您的代码做了一些实验,这是我的结论:

1-如果先放置arraySumSubFunctionCall(),然后再将arraySumInlined()放入main(),则执行时间又变了:

public static void main(String[] args) {
    ...
    arraySumSubFunctionCall(iterations,data);
    arraySumInlined(iterations,data); 
}

这意味着JIT编译器优化发生在arraySumInlined()中,然后可以应用于arraySumSubFunctionCall()。

2-如果在getSum()和arraySumInlined()中用真正的动态变量(例如new Random()。nextInt())替换了常量data.stream()。mapToInt(e-> e).sum()然后执行时间回到arraySumSubFunctionCall()和arraySumInlined()。

private static void arraySumInlined(int iterations,final ArrayList<Integer> data) {
    ...
    for (int i = 0; i < iterations; ++i) {
      result = new Random().nextInt();
    }
    ...
}


private static int getSum(final ArrayList<Integer> data) {
    return new Random().nextInt();
}

这意味着常量data.stream()。mapToInt(e-> e).sum()在arraySumInlined()中进行了优化,然后应用于arraySumSubFunctionCall()。

在现实生活中,我认为在本地for循环中重新计算N次相同值的次数并不经常发生,因此,如果代码准备就绪,则您不必担心提取方法重构。

,

对于它的价值,我也做了一些实验,发现它在静态方法中执行时,特别是与IntStream上的sum()方法配合使用的。我对您的代码进行了如下调整,以便获得每次迭代的平均持续时间:

import java.util.ArrayList;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

public class App2 {

    public static void main(String[] args) {
        final int size = 10_000;
        final int iterations = 1_000_000;
        final var data = integerListWithRandomValues(size);
        boolean inline = args.length > 0 && "inline".equalsIgnoreCase(args[0]);

        if (inline) {
            System.out.println("Running inline");
        } else {
            System.out.println("Running sub-function call");
        }

        arraySum(inline,iterations,data);
    }

    private static void arraySum(boolean inline,int iterations,final ArrayList<Integer> data) {
        long start;
        long result = 0;
        long totalElapsedTime = 0;

        for (int i = 0; i < iterations; ++i) {
            start = System.nanoTime();
            if (inline) {
                result = data.stream().mapToInt(e -> e).sum();
            } else {
                result = getIntStream(data).sum();
            }
            totalElapsedTime += getElapsedTime(start);
        }
        printElapsedTime(totalElapsedTime/iterations,result);
    }

    private static long getElapsedTime(long start) {
        return TimeUnit.NANOSECONDS.toNanos(System.nanoTime() - start);
    }

    private static void printElapsedTime(long elapsedTime,long result) {
        System.out.println(String.format("%d per iteration (%d)",elapsedTime,result));
    }

    private static IntStream getIntStream(final ArrayList<Integer> data) {
        return data.stream().mapToInt(e -> e);
    }

    private static int getSum(final ArrayList<Integer> data) {
        return data.stream().mapToInt(e -> e).sum();
    }

    private static ArrayList<Integer> integerListWithRandomValues(final int size) {
        final var result = new ArrayList<Integer>();
        final var r = new Random();

        for (int i = 0; i < size; ++i) {
            result.add(r.nextInt());
        }

        return result;
    }
}

一旦我切换到getIntStream()静态方法(尝试其他排列后),速度便与内联执行时间匹配。

本文链接:https://www.f2er.com/3098860.html

大家都在问