为什么我不能在JVM 32和64位上观察到相同的性能提升?

时间:2016-05-16 14:02:42

标签: performance jvm java-8 java-stream

我正在测试两种不同的方法(primes()primesOpt())来使用Java 8 IntStream收集前N个素数。我从Java 8 in Action的第6章中提取了这些例子。您可以从此gist Primes.java和此pom.xml获取源代码,以使用Maven和JMH集成来构建它。 (您可以将pom.xml复制到项目文件夹,将Primes.java复制到src\main\java\primes,然后使用以下命令进行构建:mvn clean install。之后,您可以使用以下命令运行基准:{{1} })。

第一个示例(java -jar target\benchmarks.jar方法)是一种将N个素数收集到primes()的简单算法。第二个(List<Integer>方法)是一种增强方法,它只测试前一个素数的除法。

我用JMH测试两个实现来计算素数的primesOpt(),最大值为10,000:

List<Integer>

根据JVM架构的不同,我获得了不同的加速比。在JVM 64位中,我发现@Benchmark public int testPrimes() { return primes(10_000).size(); } @Benchmark public int testPrimesOpt() { return primesOpt(10_000).size(); } 的速度比标准版本primesOpt()高25%,而对于JVM 32位则没有加速。

JRE 1.8.0_91-b14 64-Bit的结果:

primes()

JRE 1.8.0_91-b14 32-Bit的结果:

Benchmark              Mode  Cnt    Score    Error  Units
Primes.testPrimes     thrpt   50  269,278 ± 15,922  ops/s
Primes.testPrimesOpt  thrpt   50  341,861 ± 25,413  ops/s

这些测试是在具有双核Intel I7 Cpu的机器上进行的,具有超线程,产生2个内核和4个硬件线程。此外,该系统有4GB的RAM。使用的JVM版本是1.8.0_91-b14,在Windows 7上运行。基准测试执行时的最小堆大小为1024MB(对应于Benchmark Mode Cnt Score Error Units Primes.testPrimes thrpt 200 105,388 ± 2,741 ops/s Primes.testPrimesOpt thrpt 200 103,015 ± 2,035 ops/s )。在测量期间,没有其他活动在运行。

您是否知道为什么我不能在优化版本的质数算法上观察JVM 32位的相同性能改进?

-Xms1024M方法实现:

primes()

public static boolean isPrime(int n) { int root = (int) Math.sqrt(n); return IntStream .rangeClosed(2, root) .noneMatch(div -> n%div == 0); } public static List<Integer> primes(int max) { return IntStream .range(2, max) .filter(Primes::isPrime) .collect(ArrayList::new, ArrayList::add, ArrayList::addAll); } 方法实现:

primesOpt()

1 个答案:

答案 0 :(得分:3)

我无法重现您的结果,但通常情况下,性能会因环境因素而有很大差异。在您的代码中,takewhile方法强制处理盒装 Integer值,而非优化变体isPrime仅处理int值。< / p>

这种权衡应该能够支付您请求的更多素数,即如果扫描第一个10_000数字显示出矛盾的结果,请尝试100_0001_000_000。拳击开销在最坏的情况下是线性的,一个不错的JVM可能会将其转换为亚线性甚至是恒定的开销,而将分割限制为实际质数的改进应该高于线性,因为素数的密度随着数字的增加而下降。

所以你使用的64Bit JVM在处理盒装值时可能会有更高的开销,但我认为,使用更高max的测试也会为优化的变体带来优势 - 除非JVM知道一个技巧显着降低分工的成本。

但不应忽视您的优化变体在几个方面被打破。您正在将供应商() -> res传递给违反合同的collect,因为它不会在每次评估时返回新容器,并且收集器与前面{{1}中使用的谓词之间存在干扰}步骤。

这表明尝试优化基于Stream的解决方案可能不会导致某个地方。与以下直接方法比较:

filter

它避免了所有分区和装箱操作处于不利用Stream操作进行值选择的“劣势”,但仅用于创建最终public static List<Integer> primesBest(int max) { BitSet prime=new BitSet(); prime.set(1, max>>1); for(int i=3; i<max; i+=2) if(prime.get((i-1)>>1)) for(int b=i*3; b<max; b+=i*2) prime.clear((b-1)>>1); return IntStream.concat( IntStream.of(2), prime.stream().map(i->i+i+1)).boxed().collect(Collectors.toList()); } 。在我的机器上,它比List<Integer>元素的优化变体快10倍,10_000元素快50倍。这表明,性能差异大约为10%,20%甚至是因果二或三,不值得讨论。

我不知道如何使用Stream API表达此算法。最重要的是,并非所有操作都受益于Stream API。