Penggunaan CPU Aplikasi Java Multithreaded Terlalu Rendah pada Windows

18

Saya sedang mengerjakan aplikasi Java untuk memecahkan kelas masalah optimasi numerik - masalah pemrograman linier skala besar menjadi lebih tepat. Satu masalah dapat dipecah menjadi sub-masalah yang lebih kecil yang dapat diselesaikan secara paralel. Karena ada lebih banyak submasalah dari inti CPU, saya menggunakan ExecutorService dan mendefinisikan setiap subproblem sebagai Callable yang dikirimkan ke ExecutorService. Memecahkan subproblem membutuhkan memanggil perpustakaan asli - pemecah pemrograman linier dalam kasus ini.

Masalah

Saya dapat menjalankan aplikasi pada Unix dan pada sistem Windows dengan hingga 44 core fisik dan memori 256g, tetapi waktu komputasi pada Windows adalah urutan besarnya lebih tinggi daripada di Linux untuk masalah besar. Windows tidak hanya membutuhkan lebih banyak memori, tetapi pemanfaatan CPU dari waktu ke waktu turun dari 25% di awal menjadi 5% setelah beberapa jam. Berikut ini adalah tangkapan layar dari task manager di Windows:

Penggunaan Task Manager CPU

Pengamatan

  • Waktu solusi untuk contoh besar dari keseluruhan masalah berkisar dari berjam-jam dan menghabiskan hingga 32g memori (pada Unix). Waktu solusi untuk subproblem berada dalam kisaran ms.
  • Saya tidak menemukan masalah ini pada masalah kecil yang hanya membutuhkan waktu beberapa menit untuk menyelesaikannya.
  • Linux menggunakan kedua soket out-of-the-box, sedangkan Windows mengharuskan saya untuk secara eksplisit mengaktifkan interleaving memori di BIOS sehingga aplikasi menggunakan kedua core. Apakah saya melakukan ini tidak berpengaruh pada penurunan pemanfaatan CPU secara keseluruhan dari waktu ke waktu.
  • Ketika saya melihat utas di VisualVM semua utas kolam berjalan, tidak ada yang menunggu atau yang lain.
  • Menurut VisualVM, 90% waktu CPU dihabiskan untuk panggilan fungsi asli (menyelesaikan program linear kecil)
  • Pengumpulan Sampah tidak menjadi masalah karena aplikasi tidak membuat dan mengurangi referensi banyak objek. Juga, sebagian besar memori tampaknya dialokasikan-tumpukan. 4g tumpukan cukup di Linux dan 8g di Windows untuk contoh terbesar.

Apa yang saya coba

  • semua jenis argumen JVM, XMS tinggi, metaspace tinggi, flag UseNUMA, GC lain.
  • JVM yang berbeda (Hotspot 8, 9, 10, 11).
  • perpustakaan asli yang berbeda dari pemecah pemrograman linier yang berbeda (CLP, Xpress, Cplex, Gurobi).

Pertanyaan

  • Apa yang mendorong perbedaan kinerja antara Linux dan Windows dari aplikasi Java multi-utas besar yang banyak menggunakan panggilan asli?
  • Apakah ada sesuatu yang dapat saya ubah dalam implementasi yang akan membantu Windows, misalnya, haruskah saya menghindari menggunakan ExecutorService yang menerima ribuan Callable dan melakukan apa?
Nils
sumber
Apakah Anda mencoba ForkJoinPoolbukan ExecutorService? Utilisasi CPU 25% benar-benar rendah jika masalah Anda terkait dengan CPU.
Karol Dowbecki
1
Masalah Anda kedengarannya seperti sesuatu yang harus mendorong CPU ke 100% dan Anda berada pada 25%. Untuk beberapa masalah ForkJoinPoollebih efisien daripada penjadwalan manual.
Karol Dowbecki
2
Bersepeda melalui versi Hotspot, sudahkah Anda memastikan Anda menggunakan versi "server" dan bukan "klien"? Apa utilisasi CPU Anda di Linux? Juga, waktu operasi Windows selama beberapa hari sangat mengesankan! Apa rahasiamu? : P
erickson
3
Mungkin coba gunakan Xperf untuk menghasilkan FlameGraph . Ini bisa memberi Anda wawasan tentang apa yang dilakukan CPU (semoga mode pengguna dan kernel), tapi saya tidak pernah melakukannya di Windows.
Karol Dowbecki
1
@Nils, kedua run (unix / win) menggunakan antarmuka yang sama untuk memanggil perpustakaan asli? Saya bertanya, karena terlihat berbeda. Seperti: win menggunakan jna, linux jni.
SR

Jawaban:

2

Untuk Windows, jumlah utas per proses dibatasi oleh ruang alamat proses (lihat juga Mark Russinovich - Mendorong Batas Windows: Proses dan Utas ). Pikirkan ini menyebabkan efek samping ketika mendekati batas (memperlambat konteks, fragmentasi ...). Untuk Windows, saya akan mencoba membagi beban pekerjaan ke satu set proses. Untuk masalah serupa yang saya miliki bertahun-tahun lalu saya mengimplementasikan perpustakaan Java untuk melakukan ini dengan lebih mudah (Java 8), lihat apakah Anda suka: Perpustakaan untuk menelurkan tugas dalam proses eksternal .

Geri
sumber
Ini terlihat sangat menarik! Saya agak ragu untuk sejauh ini (belum) karena dua alasan: 1) akan ada kinerja overhead serialisasi dan mengirim objek melalui soket; 2) jika saya ingin membuat serialisasi semua ini, ini mencakup semua dependensi yang ditautkan dalam suatu tugas - akan sedikit pekerjaan untuk menulis ulang kode - meskipun demikian, terima kasih atas tautan yang bermanfaat.
Nils
Saya sepenuhnya berbagi keprihatinan Anda dan mendesain ulang kode akan menjadi upaya. Saat melintasi grafik, Anda perlu memperkenalkan ambang batas untuk jumlah utas saat saatnya membagi pekerjaan menjadi sub proses baru. Untuk alamat 2) lihat file yang dipetakan di memori Java (java.nio.MappedByteBuffer), dengan itu Anda dapat secara efektif berbagi data antar proses, misalnya data grafik Anda. Semoga berhasil :)
geri
0

Kedengarannya seperti windows caching beberapa memori untuk pagefile, setelah itu tidak tersentuh untuk beberapa waktu, dan itulah sebabnya CPU terhambat oleh kecepatan Disk

Anda dapat memverifikasinya dengan Process explorer dan memeriksa berapa banyak memori yang di-cache

Yahudi
sumber
Menurutmu? Ada cukup memori bebas. Mengapa Windows mulai bertukar? Bagaimanapun, terima kasih.
Nils
Paling tidak pada laptop saya windows menukar aplikasi yang kadang-kadang diminimalkan, bahkan dengan memori yang cukup
Jew
0

Saya pikir perbedaan kinerja ini disebabkan oleh bagaimana OS mengelola utas. JVM menyembunyikan semua perbedaan OS. Ada banyak situs di mana Anda dapat membacanya, seperti ini , misalnya. Namun bukan berarti perbedaannya menghilang.

Saya kira Anda menjalankan pada Java 8+ JVM. Karena kenyataan ini, saya sarankan Anda untuk mencoba menggunakan fitur pemrograman aliran dan fungsional. Pemrograman fungsional sangat berguna ketika Anda memiliki banyak masalah independen kecil dan Anda ingin dengan mudah beralih dari eksekusi berurutan ke paralel. Berita baiknya adalah Anda tidak perlu menetapkan kebijakan untuk menentukan berapa banyak utas yang harus Anda kelola (seperti dengan ExecutorService). Contohnya saja (diambil dari sini ):

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);
    }

}

Hasil:

Untuk streaming normal, dibutuhkan 1 menit 10 detik. Untuk aliran paralel, dibutuhkan 23 detik. PS Diuji dengan i7-7700, 16G RAM, WIndows 10

Jadi, saya sarankan Anda membaca tentang pemrograman fungsi, aliran, fungsi lambda di Jawa dan mencoba menerapkan sejumlah kecil tes dengan kode Anda (disesuaikan untuk bekerja dalam konteks baru ini).

xcesco
sumber
Saya menggunakan stream di bagian lain dari perangkat lunak, tetapi dalam kasus ini tugas dibuat saat melintasi grafik. Saya tidak akan tahu cara membungkus ini menggunakan aliran.
Nils
Bisakah Anda menelusuri grafik, membuat daftar dan kemudian menggunakan stream?
xcesco
Aliran paralel hanya gula sintaksis untuk ForkJoinPool. Itu sudah saya coba (lihat komentar @KarolDowbecki di atas).
Nils
0

Bisakah Anda memposting statistik sistem? Manajer tugas cukup baik untuk memberikan petunjuk jika itu adalah satu-satunya alat yang tersedia. Ini dapat dengan mudah mengetahui apakah tugas Anda menunggu IO - yang terdengar seperti pelakunya berdasarkan apa yang Anda jelaskan. Mungkin karena masalah manajemen memori tertentu, atau perpustakaan dapat menulis beberapa data sementara ke disk, dll.

Ketika Anda mengatakan 25% dari pemanfaatan CPU, maksud Anda hanya beberapa core yang sibuk bekerja pada saat yang sama? (Bisa jadi semua inti bekerja dari waktu ke waktu, tetapi tidak secara bersamaan.) Apakah Anda memeriksa berapa banyak utas (atau proses) yang benar-benar dibuat dalam sistem? Apakah jumlahnya selalu lebih besar dari jumlah core?

Jika ada cukup utas, apakah banyak dari mereka menganggur menunggu sesuatu? Jika benar, Anda dapat mencoba menyela (atau melampirkan debugger) untuk melihat apa yang mereka tunggu.

Xiao-Feng Li
sumber
Saya telah menambahkan tangkapan layar dari task manager untuk eksekusi yang mewakili masalah ini. Aplikasi itu sendiri menciptakan banyak utas karena ada inti fisik pada mesin. Java menyumbang sedikit lebih dari 50 utas untuk angka itu. Seperti yang sudah dikatakan VisualVM mengatakan semua utas sedang sibuk (hijau). Mereka hanya tidak mendorong CPU ke batas pada Windows. Mereka melakukannya di Linux.
Nils
@Nils Saya menduga Anda tidak benar-benar memiliki semua utas sibuk pada saat yang sama , tetapi sebenarnya hanya 9 - 10 dari mereka. Mereka dijadwalkan secara acak di semua inti, maka Anda memiliki rata-rata 9/44 = 20% pemanfaatan. Bisakah Anda menggunakan utas Java secara langsung daripada ExecutorService untuk melihat perbedaannya? Tidak sulit untuk membuat 44 utas, dan masing-masing meraih Runnable / Callable dari kumpulan tugas / antrian. (Meskipun VisualVM menunjukkan semua utas Java sedang sibuk, kenyataannya bisa jadi bahwa 44 utas dijadwalkan dengan cepat sehingga semuanya mendapatkan kesempatan untuk berjalan dalam periode pengambilan sampel VisualVM.)
Xiao-Feng Li
Itu pemikiran dan sesuatu yang sebenarnya saya lakukan di beberapa titik. Dalam implementasi saya, saya juga memastikan bahwa akses asli adalah lokal untuk setiap utas, tetapi ini tidak membuat perbedaan sama sekali.
Nils