Utilizzo CPU insufficiente dell'applicazione Java multithread su Windows


18

Sto lavorando ad un'applicazione Java per risolvere una classe di problemi di ottimizzazione numerica - problemi di programmazione lineare su larga scala per essere più precisi. Un singolo problema può essere suddiviso in sottoproblemi più piccoli che possono essere risolti in parallelo. Poiché ci sono più sottoproblemi rispetto ai core della CPU, uso un ExecutorService e definisco ogni sottoproblema come un callable che viene inviato a ExecutorService. Per risolvere un sottoproblema è necessario chiamare una libreria nativa, in questo caso un risolutore di programmazione lineare.

Problema

Posso eseguire l'applicazione su sistemi Unix e Windows con un massimo di 44 core fisici e fino a 256 g di memoria, ma i tempi di calcolo su Windows sono di un ordine di grandezza superiore rispetto a Linux per problemi di grandi dimensioni. Windows non solo richiede molta più memoria, ma l'utilizzo della CPU nel tempo scende dal 25% all'inizio al 5% dopo poche ore. Ecco uno screenshot del task manager in Windows:

Utilizzo CPU Task Manager

osservazioni

  • I tempi di soluzione per grandi istanze dell'intero problema variano da ore a giorni e consumano fino a 32 g di memoria (su Unix). I tempi di soluzione per un sottoproblema sono compresi nell'intervallo ms.
  • Non ho riscontrato questo problema su piccoli problemi che richiedono solo pochi minuti per risolverli.
  • Linux usa entrambi i socket immediatamente, mentre Windows mi richiede di attivare esplicitamente l'interleaving della memoria nel BIOS in modo che l'applicazione utilizzi entrambi i core. Se non lo faccio, non ha alcun effetto sul deterioramento dell'utilizzo complessivo della CPU nel tempo.
  • Quando guardo i thread in VisualVM tutti i thread del pool sono in esecuzione, nessuno è in attesa oppure.
  • Secondo VisualVM, il 90% del tempo CPU viene impiegato per una chiamata di funzione nativa (risoluzione di un piccolo programma lineare)
  • Garbage Collection non è un problema poiché l'applicazione non crea e de-referral molti oggetti. Inoltre, la maggior parte della memoria sembra essere allocata off-heap. 4 g di heap sono sufficienti su Linux e 8 g su Windows per l'istanza più grande.

Quello che ho provato

  • tutti i tipi di argomenti JVM, XMS elevato, metaspace elevato, flag UseNUMA, altri GC.
  • diverse JVM (Hotspot 8, 9, 10, 11).
  • diverse librerie native di diversi solutori di programmazione lineare (CLP, Xpress, Cplex, Gurobi).

Domande

  • Cosa determina la differenza di prestazioni tra Linux e Windows di una grande applicazione Java multi-thread che fa un uso pesante delle chiamate native?
  • C'è qualcosa che posso cambiare nell'implementazione che potrebbe aiutare Windows, per esempio, dovrei evitare di usare un ExecutorService che riceve migliaia di callable e fare cosa invece?

Hai provato ForkJoinPoolinvece ExecutorService? L'utilizzo della CPU del 25% è davvero basso se il tuo problema è legato alla CPU.
Karol Dowbecki,

1
Il tuo problema sembra qualcosa che dovrebbe spingere la CPU al 100% e tuttavia sei al 25%. Per alcuni problemi ForkJoinPoolè più efficiente della pianificazione manuale.
Karol Dowbecki,

2
Scorrendo le versioni di Hotspot, ti sei assicurato di utilizzare la versione "server" e non "client"? Qual è il tuo utilizzo della CPU su Linux? Inoltre, il tempo di attività di Windows di diversi giorni è impressionante! Qual è il tuo segreto? : P
erickson,

3
Forse prova a usare Xperf per generare un FlameGraph . Questo potrebbe darti un'idea di cosa sta facendo la CPU (speriamo sia in modalità utente che kernel), ma non l'ho mai fatto su Windows.
Karol Dowbecki,

1
@Nils, entrambe le esecuzioni (unix / win) utilizzano la stessa interfaccia per chiamare la libreria nativa? Chiedo, perché sembra diverso. Come: win utilizza jna, linux jni.
RS

Risposte:


2

Per Windows il numero di thread per processo è limitato dallo spazio degli indirizzi del processo (vedere anche Mark Russinovich - Pushing the Limits of Windows: Processes and Threads ). Pensa che ciò causi effetti collaterali quando si avvicina ai limiti (rallentamento dei cambi di contesto, frammentazione ...). Per Windows proverei a dividere il carico di lavoro in una serie di processi. Per un problema simile che avevo anni fa ho implementato una libreria Java per farlo più comodamente (Java 8), dai un'occhiata se ti piace: Libreria per generare attività in un processo esterno .


Questo sembra molto interessante! Sono un po 'riluttante ad andare così lontano (ancora) per due motivi: 1) ci sarà un sovraccarico prestazionale di serializzare e inviare oggetti attraverso socket; 2) se voglio serializzare tutto ciò che include tutte le dipendenze che sono collegate in un'attività - sarebbe un po 'di lavoro riscrivere il codice - tuttavia, grazie per gli utili link.
Nils,

Condivido pienamente le tue preoccupazioni e riprogettare il codice sarebbe un impegno. Mentre attraversi il grafico, dovrai introdurre una soglia per il numero di thread quando è il momento di dividere il lavoro in un nuovo processo secondario. Per indirizzare 2) dai un'occhiata al file mappato in memoria Java (java.nio.MappedByteBuffer), con il quale potresti condividere efficacemente i dati tra i processi, ad esempio i dati del tuo grafico. Godspeed :)
geri,

0

Sembra che Windows stia memorizzando nella cache un po 'di memoria per il file di paging, dopo essere rimasto intatto per qualche tempo, ed è per questo che la CPU è strozzata dalla velocità del disco

È possibile verificarlo con Process explorer e verificare la quantità di memoria memorizzata nella cache


Tu pensi? C'è abbastanza memoria libera. Perché Windows dovrebbe iniziare a scambiare? Comunque grazie.
Nils,

Almeno sul mio laptop Windows sta scambiando applicazioni a volte minimizzate, anche con memoria sufficiente
Ebreo

0

Penso che questa differenza di prestazioni sia dovuta al modo in cui il sistema operativo gestisce i thread. JVM nasconde tutte le differenze del sistema operativo. Ci sono molti siti in cui puoi leggerlo, come questo , per esempio. Ma ciò non significa che la differenza scompaia.

Suppongo che tu stia utilizzando Java 8+ JVM. Per questo motivo, ti suggerisco di provare a utilizzare lo streaming e le funzionalità di programmazione funzionale. La programmazione funzionale è molto utile quando si hanno molti piccoli problemi indipendenti e si desidera passare facilmente dall'esecuzione sequenziale a quella parallela. La buona notizia è che non è necessario definire un criterio per determinare quanti thread è necessario gestire (come con ExecutorService). Solo per esempio (preso da qui ):

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

}

Risultato:

Per flussi normali, sono necessari 1 minuto e 10 secondi. Per flussi paralleli, sono necessari 23 secondi. PS testato con i7-7700, 16G RAM, Windows 10

Quindi, ti suggerisco di leggere la programmazione delle funzioni, lo stream, la funzione lambda in Java e provare a implementare un piccolo numero di test con il tuo codice (adattato per funzionare in questo nuovo contesto).


Uso i flussi in altre parti del software, ma in questo caso le attività vengono create mentre si attraversa un grafico. Non saprei come avvolgerlo usando i flussi.
Nils,

Puoi attraversare il grafico, creare un elenco e quindi utilizzare i flussi?
xcesco,

I flussi paralleli sono solo zucchero sintattico per un ForkJoinPool. Che ho provato (vedi il commento di @KarolDowbecki sopra).
Nils,

0

Per favore, pubblichi le statistiche del sistema? Task manager è abbastanza buono da fornire qualche indizio se questo è l'unico strumento disponibile. Può facilmente capire se i tuoi compiti sono in attesa di IO - che suona come il colpevole in base a ciò che hai descritto. Potrebbe essere dovuto a determinati problemi di gestione della memoria oppure la libreria potrebbe scrivere alcuni dati temporanei sul disco, ecc.

Quando stai dicendo che il 25% dell'utilizzo della CPU, vuoi dire che solo alcuni core sono impegnati a lavorare contemporaneamente? (Può succedere che tutti i core funzionino di volta in volta, ma non contemporaneamente.) Verificheresti quanti thread (o processi) sono realmente creati nel sistema? Il numero è sempre maggiore del numero di core?

Se ci sono abbastanza thread, molti di loro sono inattivi in ​​attesa di qualcosa? Se vero, puoi provare a interrompere (o collegare un debugger) per vedere cosa stanno aspettando.


Ho aggiunto uno screenshot del task manager per un'esecuzione rappresentativa di questo problema. L'applicazione stessa crea tanti thread quanti sono i core fisici sulla macchina. Java contribuisce poco più di 50 thread a quella cifra. Come già detto, VisualVM afferma che tutti i thread sono occupati (verde). Semplicemente non spingono la CPU al limite su Windows. Lo fanno su Linux.
Nils,

@Nils Sospetto che tu non abbia davvero tutti i thread occupati allo stesso tempo, ma in realtà solo 9-10 di essi. Sono programmati casualmente su tutti i core, quindi in media hai un utilizzo del 9/44 = 20%. Puoi usare direttamente i thread Java anziché ExecutorService per vedere la differenza? Non è difficile creare 44 thread e ognuno di essi preleva Runnable / Callable da un pool di attività / coda. (Sebbene VisualVM mostri che tutti i thread Java sono occupati, la realtà può essere che i 44 thread sono programmati rapidamente in modo che tutti abbiano la possibilità di essere eseguiti nel periodo di campionamento di VisualVM.)
Xiao-Feng Li

Questo è un pensiero e qualcosa che ho effettivamente fatto ad un certo punto. Nella mia implementazione, mi sono anche assicurato che l'accesso nativo sia locale per ogni thread, ma questo non ha fatto alcuna differenza.
Nils,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.