java - 进行提取方法重构后,代码变慢了 6 倍

标签 java performance java-stream refactoring java-11

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

该程序构建了一个包含一万个随机整数的 ArrayList,然后找到元素的总和。在该示例中,求和被重复一百万次以改善耗时测量中的信噪比。在实际程序中,有一百万个略有不同的列表,但无论如何都会产生问题。

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

  • 现在,(对我而言)令人惊讶的是 arraySumInlined大约需要 7 秒,但是 arraySumSubFunctionCall大约需要 42 秒。在我看来,这是一个令人印象深刻的差异。

    如果我取消注释 arraySumInlinedarraySumSubFunctionCall然后他们每个都在大约 7 秒内完成。 IE。 arraySumSubFunctionCall不再那么慢。

    这里发生了什么?有没有更广泛的影响?例如。我以前从未想过将提取方法重构视为可以将 7 秒的方法调用变成 42 秒的方法。

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

    环境详细信息: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)",
            TimeUnit.NANOSECONDS.toMillis(end - start) / 1000.0, 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;
      }
    }
    

    最佳答案

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

    1-如果您将第一个 arraySumSubFunctionCall() 和 arraySumInlined() 放在 main() 中,则执行时间又回到不同的:

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

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

    2-如果在 getSum() 和 arraySumInlined() 中将常量 data.stream().mapToInt(e -> e).sum() 替换为真正动态的变量,例如 new Random().nextInt() 则执行时间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 次相同的值不会经常发生,所以如果代码准备需要,您不应该害怕提取方法重构。

    关于java - 进行提取方法重构后,代码变慢了 6 倍,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58867767/

    相关文章:

    java - 使用带有 JDBC Bolt 驱动程序的 Neo4j 嵌入式数据库进行 Spring Boot 测试

    java - JTable 的 TableModelListener 如何工作?

    sql - SELECT with OR 使用带索引的两列非常慢

    performance - GPU 功能会影响虚拟机性能吗?

    java - 按两个属性分组并映射到不同的对象

    java - 如果过滤值为空,如何跳过流过滤器?

    java - 根据数据库值显示单选按钮和复选框

    java - 测试类的序列化

    performance - FK 上的联接是否比没有 FK 的联接更快?

    java - 是否可以将流收集到两个收集器中