令人惊讶的 Java 表现

Surprising Java performance

我有一个 StressTester class 这样的:

public abstract class StressTest {
  public static final int WARMUP_JIT_COMPILER = 10000;

  public interface TimedAction {
    void doAction();
  }

  public static long timeAction(int numberOfTimes, TimedAction action) {
    ThreadMXBean bean = ManagementFactory.getThreadMXBean();
    for (int i = 0; i < WARMUP_JIT_COMPILER; i++) {
      action.doAction();
    }
    long currentTime = bean.getCurrentThreadCpuTime();
    for (int i = 0; i < numberOfTimes; i++) {
      action.doAction();
    }
    return (bean.getCurrentThreadCpuTime() - currentTime)/1000000;
  }
}

主要方法如下所示:

private static boolean isPrime1(int n) { ... }
private static boolean isPrime2(int n) { ... }
private static boolean isPrime3(int n) { ... }
private static boolean isPrime4(int n) { ... }

private static final int NUMBER_OF_RUNS = 1000000;

public static void main(String[] args) {
  long primeNumberFinderTime1 = StressTest.timeAction(NUMBER_OF_RUNS, () -> {
    for (int i = 0; i < 100; i++) {
      isPrime1(i);
    }
  });
  long primeNumberFinderTime2 = StressTest.timeAction(NUMBER_OF_RUNS, () -> {
    for (int i = 0; i < 100; i++) {
      isPrime2(i);
    }
  });
  long primeNumberFinderTime3 = StressTest.timeAction(NUMBER_OF_RUNS, () -> {
    for (int i = 0; i < 100; i++) {
      isPrime3(i);
    }
  });
  long primeNumberFinderTime4 = StressTest.timeAction(NUMBER_OF_RUNS, () -> {
    for (int i = 0; i < 100; i++) {
      isPrime4(i);
    }
  });
}

当我这样设置时,结果与预期的差不多,我可以交换测试,结果也按预期交换。 isPrime3isPrime1.

快大约 200 倍

我的真实代码有点复杂。我有几个 classes 可以找到这样的素数:

class PrimeNumberFinder1 {
  @Override
  bool isPrime(i) { /* same code as in static isPrime1() */ };
}

class PrimeNumberFinder2 extends PrimeNumberFinder1 {
  @Override
  bool isPrime(i) { /* same code as in static isPrime2() */ };
}

class PrimeNumberFinder3 extends PrimeNumberFinder1 {
  @Override
  bool isPrime(i) { /* same code as in static isPrime3() */ };
}

class PrimeNumberFinder4 extends PrimeNumberFinder1 {
  @Override
  bool isPrime(i) { /* same code as in static isPrime4() */ };
}

我有一个 class 这样的:

class SomeClassWithPrimeNumberFinder {
  PrimeNumberFinder1 _pnf;

  void setPrimeNumberFinder(PrimeNumberFinder1 pnf) {
    _pnf = pnf;
  }

  void stressTest() {
    StressTest.doAction(10000000, () -> {
      for (int i = 0; i < 100; i++) {
        _pnf.isPrime(i);
      }
    });
  }
}

还有我的主要方法:

public static void main(String() args) {
  SomeClassWithPrimeNumberFinder sc = new SomeClassWithPrimeNumberFinder();
  sc.setPrimeNumberFinder(new PrimeNumberFinder1());
  sc.stressTest();
  sc.setPrimeNumberFinder(new PrimeNumberFinder2());
  sc.stressTest();
  sc.setPrimeNumberFinder(new PrimeNumberFinder3());
  sc.stressTest();
  sc.setPrimeNumberFinder(new PrimeNumberFinder4());
  sc.stressTest();
}

使用此设置 PrimeNumberFind1 与第一个测试中的 isPrime1() 差不多快。但是 PrimeNumberFind3 在第一个测试中比 isPrime3() 慢了大约 200 倍。

如果我移动 PrimeNumberFind3 使其先运行,我在第一次测试中获得与 isPrime3() 相同的时间。其余时间也有点慢 (5-10%),但不像 PrimeNumberFind3.

前 3 个 PrimeNumberFind 只是循环和 ifs。没有国家参与。最后一个有一个创建查找列表的构造函数,但也只是一个简单的循环。如果我从构造函数中取出代码并使用数组文字创建查找列表,则时间是相同的。

知道为什么会这样吗?

可能发生的情况是,最初 isPrime 作为死代码被丢弃,因为结果不是 kept/used,所以它将 运行 快得不可思议,即比时钟快例如循环。

但是,当您提供多个实现时,它必须选择调用哪个方法,因此使用第二种方法需要更长的时间,但 JIT 不能内联两个以上的方法,因此第三个实现意味着测试是显着变慢,因为它必须做更多的工作来调用丢弃的方法。