为什么我不能在 32 位和 64 位 JVM 上观察到相同的性能改进?
Why can’t I observe the same performance improvement on both JVMs 32 and 64bits?
我正在测试两种不同的方法(primes()
和 primesOpt()
)来使用 Java 8 IntStream
收集前 N 个质数。我从 Java 8 in Action. You can get the source code from this gist Primes.java and this pom.xml 的第 6 章中获取了这些示例,以使用 Maven 和 JMH 集成来构建它。 (你可以将 pom.xml
复制到项目文件夹,将 Primes.java
复制到 src\main\java\primes
并使用命令构建它:mvn clean install
。之后你可以 运行 基准测试: java -jar target\benchmarks.jar
).
第一个示例(primes()
方法)是一种将 N 个素数收集到 List<Integer>
中的简单算法。第二种方法(primesOpt()
方法)是一种增强方法,它只测试除以前面的质数。
我用 JMH 测试了这两个实现,以计算 List<Integer>
个素数,最大值为 10,000:
@Benchmark
public int testPrimes() {
return primes(10_000).size();
}
@Benchmark
public int testPrimesOpt() {
return primesOpt(10_000).size();
}
而且我根据 JVM 架构获得了不同的加速。在 JVM 64 位中,我观察到 primesOpt()
比标准版本 primes()
加速了 25%,而对于 JVM 32 位,没有加速。
JRE 1.8 的结果。0_91-b14 64 位:
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
JRE 1.8 的结果。0_91-b14 32 位:
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
这些测试是在具有双核 Intel I7 Cpu 的机器上执行的,具有超线程,导致 2 个内核和 4 个硬件线程。此外,系统有 4GB 的 RAM。使用的 JVM 版本是 Windows 7 上的 1.8.0_91-b14、运行ning。基准测试以最小堆大小 1024MB(对应于 -Xms1024M
)执行。在测量期间没有其他活动 运行ning。
你知道为什么我不能在 JVM 32 位上观察到素数算法优化版本的相同性能改进吗?
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()
方法实现:
public static boolean isPrimeOpt(List<Integer> primes, int n) {
int root = (int) Math.sqrt(n);
return takeWhile(primes, root)
.stream()
.noneMatch(div -> n%div == 0);
}
public static List<Integer> takeWhile(List<Integer> src, int max) {
int i;
for(i = 0; i < src.size() && src.get(i) <= max; i++) {}
return src.subList(0, i);
}
public static List<Integer> primesOpt(int max) {
ArrayList<Integer> res = new ArrayList<>();
return IntStream
.range(2, max)
.filter(n -> Primes.isPrimeOpt(res, n))
.collect(() -> res, ArrayList::add, (l1, l2) -> {});
}
我无法重现您的结果,但通常情况下,性能会因环境因素而有很大差异。在您的代码中,takewhile
方法强制处理 boxed Integer
值,而未优化的变体 isPrime
正在处理 int
值只有.
您要求的素数越多,这种权衡就会得到回报,即如果扫描第一个 10_000
个数字显示矛盾的结果,请尝试 100_000
或 1_000_000
。装箱开销在最坏的情况下是线性的,一个体面的 JVM 可能会将其变成次线性甚至恒定的开销,而将除法限制为实际素数的改进应该高于线性,因为素数的密度随着数字的增加而下降。
因此,也许您使用的 64 位 JVM 在处理装箱值时具有更高的开销,但我认为,使用更高的 max
进行测试也会揭示您的优化变体的优势——除非 JVM 知道显着降低除法运算成本的技巧。
但不容忽视的是,您的优化变体在多个方面存在问题。您正在将供应商 () -> res
传递给 collect
,这违反了合同,因为它在每次评估时都没有 return 一个新容器,并且 Collector 和前面使用的 Predicate 之间存在干扰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());
}
它避免了所有除法和装箱操作的“缺点”,即不使用 Stream 操作进行值选择,而仅用于创建最终 List<Integer>
。在我的机器上,它比 10_000
元素的优化变体快 10 倍,1_000_000
元素快 50 倍。这表明 10%、20% 甚至两三倍的性能差异不值得任何讨论。
虽然我不明白如何使用 Stream API 来表达这个算法。底线可能是并非每个操作都受益于 Stream API.
我正在测试两种不同的方法(primes()
和 primesOpt()
)来使用 Java 8 IntStream
收集前 N 个质数。我从 Java 8 in Action. You can get the source code from this gist Primes.java and this pom.xml 的第 6 章中获取了这些示例,以使用 Maven 和 JMH 集成来构建它。 (你可以将 pom.xml
复制到项目文件夹,将 Primes.java
复制到 src\main\java\primes
并使用命令构建它:mvn clean install
。之后你可以 运行 基准测试: java -jar target\benchmarks.jar
).
第一个示例(primes()
方法)是一种将 N 个素数收集到 List<Integer>
中的简单算法。第二种方法(primesOpt()
方法)是一种增强方法,它只测试除以前面的质数。
我用 JMH 测试了这两个实现,以计算 List<Integer>
个素数,最大值为 10,000:
@Benchmark
public int testPrimes() {
return primes(10_000).size();
}
@Benchmark
public int testPrimesOpt() {
return primesOpt(10_000).size();
}
而且我根据 JVM 架构获得了不同的加速。在 JVM 64 位中,我观察到 primesOpt()
比标准版本 primes()
加速了 25%,而对于 JVM 32 位,没有加速。
JRE 1.8 的结果。0_91-b14 64 位:
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
JRE 1.8 的结果。0_91-b14 32 位:
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
这些测试是在具有双核 Intel I7 Cpu 的机器上执行的,具有超线程,导致 2 个内核和 4 个硬件线程。此外,系统有 4GB 的 RAM。使用的 JVM 版本是 Windows 7 上的 1.8.0_91-b14、运行ning。基准测试以最小堆大小 1024MB(对应于 -Xms1024M
)执行。在测量期间没有其他活动 运行ning。
你知道为什么我不能在 JVM 32 位上观察到素数算法优化版本的相同性能改进吗?
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()
方法实现:
public static boolean isPrimeOpt(List<Integer> primes, int n) {
int root = (int) Math.sqrt(n);
return takeWhile(primes, root)
.stream()
.noneMatch(div -> n%div == 0);
}
public static List<Integer> takeWhile(List<Integer> src, int max) {
int i;
for(i = 0; i < src.size() && src.get(i) <= max; i++) {}
return src.subList(0, i);
}
public static List<Integer> primesOpt(int max) {
ArrayList<Integer> res = new ArrayList<>();
return IntStream
.range(2, max)
.filter(n -> Primes.isPrimeOpt(res, n))
.collect(() -> res, ArrayList::add, (l1, l2) -> {});
}
我无法重现您的结果,但通常情况下,性能会因环境因素而有很大差异。在您的代码中,takewhile
方法强制处理 boxed Integer
值,而未优化的变体 isPrime
正在处理 int
值只有.
您要求的素数越多,这种权衡就会得到回报,即如果扫描第一个 10_000
个数字显示矛盾的结果,请尝试 100_000
或 1_000_000
。装箱开销在最坏的情况下是线性的,一个体面的 JVM 可能会将其变成次线性甚至恒定的开销,而将除法限制为实际素数的改进应该高于线性,因为素数的密度随着数字的增加而下降。
因此,也许您使用的 64 位 JVM 在处理装箱值时具有更高的开销,但我认为,使用更高的 max
进行测试也会揭示您的优化变体的优势——除非 JVM 知道显着降低除法运算成本的技巧。
但不容忽视的是,您的优化变体在多个方面存在问题。您正在将供应商 () -> res
传递给 collect
,这违反了合同,因为它在每次评估时都没有 return 一个新容器,并且 Collector 和前面使用的 Predicate 之间存在干扰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());
}
它避免了所有除法和装箱操作的“缺点”,即不使用 Stream 操作进行值选择,而仅用于创建最终 List<Integer>
。在我的机器上,它比 10_000
元素的优化变体快 10 倍,1_000_000
元素快 50 倍。这表明 10%、20% 甚至两三倍的性能差异不值得任何讨论。
虽然我不明白如何使用 Stream API 来表达这个算法。底线可能是并非每个操作都受益于 Stream API.