【问题标题】:Too Low CPU Usage of Multithreaded Java Application on WindowsWindows 上多线程 Java 应用程序的 CPU 使用率过低
【发布时间】:2020-03-10 22:58:35
【问题描述】:

我正在开发一个 Java 应用程序,用于解决一类数值优化问题——更准确地说是大规模线性规划问题。单个问题可以拆分为可以并行解决的较小子问题。由于子问题比 CPU 内核多,因此我使用 ExecutorService 并将每个子问题定义为可提交给 ExecutorService 的 Callable。解决子问题需要调用本机库——在这种情况下是线性规划求解器。

问题

我可以在 Unix 和具有多达 44 个物理内核和多达 256g 内存的 Windows 系统上运行该应用程序,但对于大型问题,Windows 上的计算时间比 Linux 上高一个数量级。 Windows 不仅需要更多的内存,而且随着时间的推移,CPU 利用率从开始时的 25% 下降到几个小时后的 5%。下面是 Windows 中任务管理器的截图:

观察

  • 整体问题的大型实例的解决时间从数小时到数天不等,最多消耗 32g 内存(在 Unix 上)。子问题的求解时间在毫秒范围内。
  • 我不会在只需几分钟即可解决的小问题上遇到此问题。
  • Linux 使用开箱即用的两个套接字,而 Windows 要求我在 BIOS 中显式激活内存交错,以便应用程序利用两个内核。不过,无论我是否这样做,都不会影响整体 CPU 利用率随着时间的推移而下降。
  • 当我查看 VisualVM 中的线程时,所有池线程都在运行,没有一个处于等待状态。
  • 根据 VisualVM,90% 的 CPU 时间花在原生函数调用上(解决小型线性程序)
  • 垃圾收集不是问题,因为应用程序不会创建和取消引用很多对象。此外,大多数内存似乎是在堆外分配的。对于最大的实例,Linux 上 4g 的堆就足够了,Windows 上 8g 就足够了。

我的尝试

  • 各种 JVM 参数、高 XMS、高元空间、UseNUMA 标志、其他 GC。
  • 不同的 JVM(热点 8、9、10、11)。
  • 不同线性规划求解器(CLP、Xpress、Cplex、Gurobi)的不同本地库。

问题

  • 大量使用本机调用的大型多线程 Java 应用程序导致 Linux 和 Windows 之间的性能差异是什么原因?
  • 我可以在实现中进行哪些更改以帮助 Windows,例如,我是否应该避免使用接收数千个 Callables 的 ExecutorService 并改为执行哪些操作?

【问题讨论】:

  • 你试过ForkJoinPool而不是ExecutorService吗?如果您的问题是 CPU 受限,那么 25% 的 CPU 利用率真的很低。
  • 您的问题听起来像是应该将 CPU 提高到 100% 的问题,但您却处于 25%。对于一些问题ForkJoinPool比人工调度效率更高。
  • 循环浏览热点版本,您确定您使用的是“服务器”版本而不是“客户端”版本吗?你在 Linux 上的 CPU 利用率是多少?此外,几天的 Windows 正常运行时间令人印象深刻!你的秘密是什么? :P
  • 也许可以尝试使用Xperf 生成FlameGraph。这可以让您了解 CPU 在做什么(希望是用户模式和内核模式),但我从未在 Windows 上这样做过。
  • @Nils,两个运行(unix/win)都使用相同的接口来调用本机库?我问,因为它看起来不一样。比如:win用jna,linux jni。

标签: java multithreading java-native-interface jvm-hotspot numa


【解决方案1】:

对于 Windows,每个进程的线程数受进程地址空间的限制(另请参阅 Mark Russinovich - Pushing the Limits of Windows: Processes and Threads)。认为这会在接近极限时产生副作用(上下文切换速度减慢、碎片化......)。对于 Windows,我会尝试将工作负载分配给一组进程。对于我多年前遇到的类似问题,我实现了一个 Java 库来更方便地执行此操作(Java 8),如果您喜欢,请查看:Library to spawn tasks in an external process

【讨论】:

  • 这看起来很有趣!我有点犹豫(至今)有两个原因:1)通过套接字序列化和发送对象会产生性能开销; 2)如果我想序列化包括任务中链接的所有依赖项的所有内容 - 重写代码需要一些工作 - 不过,感谢您提供有用的链接。
  • 我完全同意您的担忧,重新设计代码将是一些努力。在遍历图表时,您需要在将工作拆分为新的子进程时引入线程数量的阈值。要解决 2) 查看 Java 内存映射文件 (java.nio.MappedByteBuffer),您可以有效地在进程之间共享数据,例如您的图形数据。神速:)
【解决方案2】:

听起来windows正在缓存一些内存到页面文件,在它没有被触及一段时间后,这就是为什么CPU受到磁盘速度的瓶颈

您可以使用进程资源管理器验证它并检查缓存了多少内存

【讨论】:

  • 你觉得呢?有足够的可用内存。为什么 Windows 会开始交换?无论如何,谢谢。
  • 至少在我的笔记本电脑上,窗口有时会交换最小化的应用程序,即使有足够的内存
【解决方案3】:

我认为这种性能差异是由于操作系统如何实现的。管理线程。 JVM 隐藏所有操作系统差异。您可以在许多网站上了解它,例如this。但这并不意味着差异消失了。

我想你在 Java 8+ JVM 上运行。由于这个事实,我建议你尝试使用流和函数式编程特性。当您有许多小的独立问题并且希望轻松地从顺序执行切换到并行执行时,函数式编程非常有用。好消息是您不必定义策略来确定您必须管理多少线程(例如使用 ExecutorService)。举个例子(取自here):

package com.mkyong.java8;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class ParallelExample4 {

    public static void main(String[] args) {

        long count = Stream.iterate(0, n -> n + 1)
                .limit(1_000_000)
                //.parallel()   with this 23s, without this 1m 10s
                .filter(ParallelExample4::isPrime)
                .peek(x -> System.out.format("%s\t", x))
                .count();

        System.out.println("\nTotal: " + count);

    }

    public static boolean isPrime(int number) {
        if (number <= 1) return false;
        return !IntStream.rangeClosed(2, number / 2).anyMatch(i -> number % i == 0);
    }

}

结果:

对于普通流,需要 1 分 10 秒。对于并行 流,需要 23 秒。 P.S 使用 i7-7700、16G RAM 测试, 窗口 10

所以,我建议你阅读 Java 中的函数编程、流、lambda 函数,并尝试用你的代码实现少量测试(适应在这种新环境中工作)。

【讨论】:

  • 我在软件的其他部分使用流,但在这种情况下,任务是在遍历图形时创建的。我不知道如何使用流来包装它。
  • 能不能遍历图,建立列表,然后使用流?
  • 并行流只是 ForkJoinPool 的语法糖。我已经尝试过(参见上面的@KarolDowbecki 评论)。
【解决方案4】:

请您发布系统统计信息吗?如果这是唯一可用的工具,任务管理器足以提供一些线索。它可以轻松判断您的任务是否正在等待 IO - 根据您的描述,这听起来像是罪魁祸首。这可能是由于某些内存管理问题,或者库可能会将一些临时数据写入磁盘等。

当您说 25% 的 CPU 利用率时,您的意思是只有几个内核同时忙于工作吗? (可能是所有内核不时工作,但不是同时工作。)您会检查系统中真正创建了多少线程(或进程)吗?数字总是大于核心数吗?

如果有足够多的线程,它们中的许多是空闲等待什么吗?如果为真,您可以尝试中断(或附加调试器)以查看它们在等待什么。

【讨论】:

  • 我已经为代表这个问题的执行添加了任务管理器的屏幕截图。应用程序本身会创建与机器上的物理内核一样多的线程。 Java 为这个数字贡献了 50 多个线程。正如已经说过的,VisualVM 说所有线程都忙(绿色)。他们只是没有将 CPU 推到 Windows 的极限。他们在 Linux 上做。
  • @Nils 我怀疑您并没有真正让所有线程同时同时忙碌,但实际上只有 9 - 10 个。它们在所有内核中随机调度,因此平均利用率为 9/44 = 20%。你可以直接使用Java线程而不是ExecutorService来看看区别吗?创建 44 个线程并不难,每个线程都从任务池/队列中获取 Runnable/Callable。 (虽然 VisualVM 显示所有 Java 线程都很忙,但实际情况可能是这 44 个线程被快速调度,以便它们都有机会在 VisualVM 的采样周期内运行。)
  • 这是一个想法,也是我在某个时候实际做过的事情。在我的实现中,我还确保本地访问对每个线程都是本地的,但这根本没有区别。
猜你喜欢
  • 2015-11-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多