令人惊讶的 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);
}
});
}
当我这样设置时,结果与预期的差不多,我可以交换测试,结果也按预期交换。 isPrime3
比 isPrime1
.
快大约 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 不能内联两个以上的方法,因此第三个实现意味着测试是显着变慢,因为它必须做更多的工作来调用丢弃的方法。
我有一个 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);
}
});
}
当我这样设置时,结果与预期的差不多,我可以交换测试,结果也按预期交换。 isPrime3
比 isPrime1
.
我的真实代码有点复杂。我有几个 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 不能内联两个以上的方法,因此第三个实现意味着测试是显着变慢,因为它必须做更多的工作来调用丢弃的方法。