【问题标题】:Why can’t I observe the same performance improvement on both JVMs 32 and 64bits?为什么我不能在 32 位和 64 位 JVM 上观察到相同的性能改进?
【发布时间】:2016-05-16 14:02:41
【问题描述】:

我正在测试两种不同的方法(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。之后您可以使用: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 架构,我得到了不同的加速。在 64 位 JVM 中,我观察到 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 版本是 1.8.0_91-b14,在 Windows 7 上运行。基准测试以 1024MB 的最小堆大小执行(对应于-Xms1024M)。在测量期间没有其他活动在运行。

您知道为什么我不能在 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) -> {});
}

【问题讨论】:

  • 我建议您使用 JMH 来验证您的微基准。如果你得到奇怪的结果,很难说测试中是否存在真正的错误差异。
  • @PeterLawrey 我用 JMH 结果更新了 OP。我没有观察到两倍的加速,但我仍然观察到 JVM 的 64 位和 32 位版本之间的结果不一致。 primesOpt() 在 JVM 64 位上的性能提高了 20%,但在 JVM 32 位上几乎差了 10%。这个结果仍然是我提出问题的原因:为什么我不能在 32 位和 64 位 JVM 上观察到相同的性能提升?
  • 我认为您应该从 profiling 两个平台上的基准测试开始。另外,打开 GC 日志记录。
  • 你的内存设置是什么? GC 日志是什么样的?您的 PC 是否 100% 运行,磁盘交换等。在性能方面,您需要查看完整的环境设置以确保没有瓶颈。
  • collect(() -&gt; res, ArrayList::add, (l1, l2) -&gt; {}) 是对 API 的完全破坏性滥用。由于您实际上正在执行与forEachOrdered(res::add) 等效的操作,因此请使用它。

标签: performance jvm java-8 java-stream


【解决方案1】:

我无法重现您的结果,但通常情况下,性能可能会因环境因素而显着不同。在您的代码中,takewhile 方法强制处理 boxed Integer 值,而非优化变体 isPrime 仅处理 int 值。

这种权衡应该会得到您请求的更多质数的回报,即如果扫描第一个 10_000 数字显示矛盾的结果,请尝试 100_0001_000_000。装箱开销在最坏的情况下是线性的,体面的 JVM 可能会将其变成亚线性甚至恒定的开销,而将除法限制为实际素数的改进应该高于线性,因为素数的密度随着数字的增加而下降。

因此,您使用的 64 位 JVM 在处理装箱值时可能具有更高的开销,但我认为,使用更高的 max 进行测试也会显示优化变体的优势——除非 JVM 知道减少的技巧部门运营成本显着。


但不容忽视的是,您的优化变体在几个方面被破坏了。您将供应商() -&gt; res 传递给collect,这违反了合同,因为它不会在每次评估时返回新容器,并且在前面的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&lt;Integer&gt;。在我的机器上,它比 10_000 元素的优化变体快大约 10 倍,1_000_000 元素的优化变体快 50 倍。这表明 10%、20% 甚至两倍或三倍的性能差异不值得讨论。

不过,我不明白如何使用 Stream API 来表达这个算法。底线可能是并非所有操作都能从 Stream API 中受益。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-11-27
    • 2010-10-28
    • 2011-09-12
    • 2011-06-26
    • 2017-04-22
    • 2012-09-19
    相关资源
    最近更新 更多