WindowsでのマルチスレッドJavaアプリケーションのCPU使用率が低すぎる


18

私は、数値最適化問題のクラスを解決するためのJavaアプリケーションに取り組んでいます-より正確に言うと、大規模線形計画問題です。単一の問題は、並列に解決できる小さなサブ問題に分割できます。CPUコアよりも多くのサブ問題があるため、ExecutorServiceを使用して、各サブ問題を、ExecutorServiceに送信されるCallableとして定義します。サブ問題を解決するには、ネイティブライブラリ(この場合は線形計画法ソルバー)を呼び出す必要があります。

問題

最大44の物理コアと最大256gのメモリを備えたUnixおよびWindowsシステムでアプリケーションを実行できますが、大きな問題の場合、Windowsでの計算時間はLinuxでの計算時間よりも桁違いに長くなります。Windowsはかなり多くのメモリを必要とするだけでなく、時間の経過に伴うCPU使用率は最初の25%から数時間後には5%に低下します。Windowsのタスクマネージャのスクリーンショットは次のとおりです。

タスクマネージャのCPU使用率

観察

  • 問題全体の大規模なインスタンスの解決時間は数時間から数日の範囲で、最大32gのメモリを消費します(UNIXの場合)。副問題の解決時間はmsの範囲です。
  • この問題は、数分で解決できる小さな問題では発生しません。
  • 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に役立つ実装で変更できるものはありますか。たとえば、何千ものCallableを受け取るExecutorServiceの使用を避け、代わりに何をすべきでしょうか?

ForkJoinPool代わりに試しましたExecutorServiceか?問題がCPUバウンドの場合、25%のCPU使用率は本当に低くなります。
Karol Dowbecki

1
あなたの問題は、CPUを100%に押し上げるもののように聞こえますが、25%です。一部の問題についてForkJoinPoolは、手動スケジューリングよりも効率的です。
Karol Dowbecki

2
ホットスポットバージョンを循環して、「クライアント」バージョンではなく「サーバー」バージョンを使用していることを確認しましたか?LinuxでのCPU使用率はどれくらいですか?また、Windowsの数日間のアップタイムも印象的です。あなたの秘密は何ですか?:P
エリクソン

3
Xperfを使用してFlameGraphを生成してみてください。これは、CPUが何をしているのか(できればユーザーモードとカーネルモードの両方)を理解するのに役立ちますが、Windowsでは実行していません。
Karol Dowbecki

1
@Nils、両方の実行(Unix / Win)が同じインターフェイスを使用してネイティブライブラリを呼び出しますか?見た目が違うのでお願いします。Like:winはjna、linux jniを使用します。
SR

回答:


2

Windowsの場合、プロセスあたりのスレッド数はプロセスのアドレス空間によって制限されます(Mark Russinovich-Pushing the Limits of Windows:Processes and Threadsも参照)。これが限界に近づくと、副作用が発生すると考えてください(コンテキストスイッチのスローダウン、断片化...)。Windowsの場合、作業負荷を一連のプロセスに分割しようとします。数年前に私がこれをより便利に実行するためにJavaライブラリを実装した同様の問題(Java 8)については、次のように見てください。外部プロセスでタスクを生成するライブラリ


これは非常に興味深いですね。次の2つの理由により、(まだ)これまでのところ、少し躊躇しています。1)ソケットを介してオブジェクトをシリアル化および送信すると、パフォーマンスのオーバーヘッドが発生します。2)すべてをシリアル化する場合、タスクにリンクされているすべての依存関係が含まれます-コードを書き直すのは少し面倒ですが、それでも有用なリンクをありがとうございます。
Nils

私はあなたの懸念を完全に共有し、コードの再設計はいくつかの努力になるでしょう。グラフをトラバースする際に、作業を新しいサブプロセスに分割するときは、スレッド数のしきい値を導入する必要があります。対処するには2)Javaのメモリマップファイル(java.nio.MappedByteBuffer)を見てください。これにより、プロセス間でデータ(グラフデータなど)を効果的に共有できます。Godspeed :)
ジェリー

0

Windowsのように聞こえるのは、しばらくの間何の操作もされていないのに、メモリをページファイルにキャッシュしているため、ディスク速度によってCPUがボトルネックになる理由です。

プロセスエクスプローラーでそれを確認し、キャッシュされているメモリの量を確認できます


あなたは考える?十分な空きメモリがあります。Windowsがスワップを開始するのはなぜですか?とにかく、ありがとう。
Nils

少なくとも私のラップトップウィンドウで、十分なメモリがある場合でも、最小化されたアプリケーションがスワップされることがあります
Jew

0

このパフォーマンスの違いは、OSがスレッドを管理する方法によるものだと思います。JVMはすべてのOSの違いを隠します。このようにそれについて読むことができる多くのサイトがあります。しかし、違いがなくなるわけではありません。

あなたはJava 8+ JVMで実行していると思います。このため、ストリームおよび関数型プログラミング機能を使用することをお勧めします。関数型プログラミングは、小さな独立した問題が多く、シーケンシャル実行からパラレル実行に簡単に切り替えたい場合に非常に役立ちます。良い知らせは、(ExecutorServiceのように)管理する必要があるスレッドの数を決定するポリシーを定義する必要がないことです。例として(ここから取得):

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秒かかります。PSはi7-7700、16G RAM、Windows 10でテスト済み

そのため、Javaでの関数プログラミング、ストリーム、ラムダ関数について読み、コードを使用して少数のテストを実装することをお勧めします(この新しいコンテキストで動作するように適応されています)。


ソフトウェアの他の部分でストリームを使用していますが、この場合、グラフをトラバースしながらタスクが作成されます。ストリームを使用してこれをラップする方法がわかりません。
Nils

グラフをトラバースし、リストを作成してから、ストリームを使用できますか?
xcesco

並列ストリームは、ForkJoinPoolの構文上の砂糖にすぎません。私が試したこと(上記の@KarolDowbeckiコメントを参照)。
Nils

0

システム統計を投稿していただけませんか?タスクマネージャーは、それが利用可能な唯一のツールである場合、いくつかの手掛かりを提供するのに十分です。タスクがIOを待っているかどうかを簡単に判断できます。これは、あなたが説明した内容に基づいて犯人のように聞こえます。特定のメモリ管理の問題が原因であるか、ライブラリが一時データをディスクに書き込むなどの可能性があります。

CPU使用率の25%と言っているのは、同時に動作しているコアが数コアしかないということですか?(すべてのコアが時々動作するが、同時に動作しない場合があります。)システムで実際に作成されているスレッド(またはプロセス)の数を確認しますか?数は常にコアの数よりも大きいですか?

十分なスレッドがある場合、それらの多くはアイドル状態で何かを待っていますか?trueの場合は、割り込み(またはデバッガーを接続)して、それらが待機しているものを確認できます。


この問題を表す実行のタスクマネージャのスクリーンショットを追加しました。アプリケーション自体は、マシン上の物理コアと同じ数のスレッドを作成します。Javaは、この数値に50を少し超えるスレッドを提供しています。すでに述べたように、VisualVMはすべてのスレッドがビジー(緑色)であると言っています。WindowsでCPUを限界まで押し上げていないだけです。彼らはLinuxでそうします。
Nils

@Nils私はあなたが同時にすべてのスレッドをビジーにしていないと思いますが、実際には9-10しかありません。それらはすべてのコアにわたってランダムにスケジュールされるため、平均で9/44 = 20%の使用率になります。違いを確認するには、ExecutorServiceではなくJavaスレッドを直接使用できますか?44のスレッドを作成し、それぞれがタスクプール/キューからRunnable / Callableを取得することは難しくありません。(VisualVMのは、すべてのJavaスレッドがビジーで示しているが、現実には、44件のスレッドがそれらのすべてがVisualVMののサンプリング周期で実行するチャンスを得るようにすることをすぐに予定されていることをすることができます。)
シャオ風水李

それは私が実際にある時点で行った考えと何かです。私の実装では、ネイティブアクセスが各スレッドに対してローカルであることも確認しましたが、これはまったく違いがありませんでした。
Nils
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.