Uso de CPU muito baixo do aplicativo Java multithread no Windows

18

Estou trabalhando em um aplicativo Java para resolver uma classe de problemas de otimização numérica - problemas de programação linear em larga escala para ser mais preciso. Um único problema pode ser dividido em subproblemas menores que podem ser resolvidos em paralelo. Como existem mais subproblemas do que núcleos de CPU, eu uso um ExecutorService e defino cada subproblema como Callable que é enviado ao ExecutorService. A solução de um subproblema requer a chamada de uma biblioteca nativa - neste caso, um solucionador de programação linear.

Problema

Posso executar o aplicativo nos sistemas Unix e Windows com até 44 núcleos físicos e até 256g de memória, mas os tempos de computação no Windows são uma ordem de magnitude maior que no Linux para grandes problemas. O Windows não apenas requer substancialmente mais memória, mas a utilização da CPU ao longo do tempo cai de 25% no início para 5% após algumas horas. Aqui está uma captura de tela do gerenciador de tarefas no Windows:

Utilização da CPU do Gerenciador de Tarefas

Observações

  • Os tempos de solução para grandes instâncias do problema geral variam de horas a dias e consomem até 32g de memória (no Unix). Os tempos de solução para um subproblema estão na faixa de ms.
  • Não encontro esse problema em pequenos problemas que levam apenas alguns minutos para serem resolvidos.
  • O Linux usa os dois soquetes prontos para uso, enquanto o Windows exige que eu ative explicitamente a intercalação de memória no BIOS para que o aplicativo utilize os dois núcleos. Independentemente de eu não fazer isso, isso não afeta a deterioração da utilização geral da CPU ao longo do tempo.
  • Quando olho para os threads no VisualVM, todos os threads do pool estão em execução, nenhum está em espera ou então.
  • De acordo com o VisualVM, 90% do tempo da CPU é gasto em uma chamada de função nativa (resolvendo um pequeno programa linear)
  • A Coleta de Lixo não é um problema, pois o aplicativo não cria e não faz referência a muitos objetos. Além disso, a maioria da memória parece estar alocada fora da pilha. 4g de heap são suficientes no Linux e 8g no Windows para a maior instância.

O que eu tentei

  • todos os tipos de argumentos da JVM, alto XMS, alto metasspace, sinalizador UseNUMA e outros GCs.
  • JVMs diferentes (ponto de acesso 8, 9, 10, 11).
  • diferentes bibliotecas nativas de diferentes solucionadores de programação linear (CLP, Xpress, Cplex, Gurobi).

Questões

  • O que impulsiona a diferença de desempenho entre o Linux e o Windows de um aplicativo Java multiencadeado grande que faz uso intenso de chamadas nativas?
  • Existe algo que eu possa alterar na implementação que ajude o Windows, por exemplo, devo evitar o uso de um ExecutorService que receba milhares de chamadas e faça o que?
Nils
fonte
Você já tentou em ForkJoinPoolvez de ExecutorService? A utilização de 25% da CPU é muito baixa se o problema estiver ligado à CPU.
Karol Dowbecki 14/11/19
11
Seu problema parece algo que deve levar a CPU a 100% e, no entanto, você está com 25%. Para alguns problemas, ForkJoinPoolé mais eficiente que o agendamento manual.
Karol Dowbecki 14/11/19
2
Passando pelas versões do Hotspot, você se certificou de estar usando a versão "server" e não "client"? Qual é a sua utilização de CPU no Linux? Além disso, o tempo de atividade do Windows de vários dias é impressionante! Qual é o seu segredo? : P
erickson
3
Talvez tente usar o Xperf para gerar um FlameGraph . Isso pode lhe dar uma ideia do que a CPU está fazendo (espero que seja o modo usuário e kernel), mas eu nunca fiz isso no Windows.
Karol Dowbecki
11
@ Nils, ambas as execuções (unix / win) usam a mesma interface para chamar a biblioteca nativa? Eu pergunto, porque parece diferente. Como: win usa jna, linux jni.
SR

Respostas:

2

No Windows, o número de threads por processo é limitado pelo espaço de endereço do processo (consulte também Mark Russinovich - Pressionando os limites do Windows: processos e threads ). Pense que isso causa efeitos colaterais quando se aproxima dos limites (desaceleração das alternâncias de contexto, fragmentação ...). Para o Windows, tentaria dividir a carga de trabalho em um conjunto de processos. Para um problema semelhante ao que eu tinha anos atrás, implementei uma biblioteca Java para fazer isso de maneira mais conveniente (Java 8), veja se você gosta: Biblioteca para gerar tarefas em um processo externo .

geri
fonte
Isso parece muito interessante! Estou um pouco hesitante em ir até aqui (ainda) por dois motivos: 1) haverá uma sobrecarga de desempenho na serialização e no envio de objetos pelos soquetes; 2) se eu quiser serializar tudo, isso inclui todas as dependências vinculadas em uma tarefa - seria um pouco trabalhoso reescrever o código - mesmo assim, obrigado pelos links úteis.
Nils
Compartilho plenamente suas preocupações e redesenhar o código seria um esforço. Ao percorrer o gráfico, você precisará introduzir um limite para o número de threads quando chegar a hora de dividir o trabalho em um novo subprocesso. Para abordar 2), dê uma olhada no arquivo mapeado na memória Java (java.nio.MappedByteBuffer), com o qual você poderá compartilhar dados entre processos de maneira eficaz, por exemplo, dados do gráfico. Godspeed :)
geri
0

Parece que o Windows está armazenando em cache alguma memória no arquivo de paginação, depois de ter sido tocado por algum tempo, e é por isso que a CPU está afunilada pela velocidade do disco

Você pode verificá-lo com o Process Explorer e verificar quanta memória está armazenada em cache

judeu
fonte
Você pensa? Há memória livre suficiente. Por que o Windows começaria a trocar? De qualquer forma, obrigado.
Nils
Pelo menos no meu Windows laptop está trocando aplicações vezes minimizados, mesmo com memória suficiente
judeu
0

Eu acho que essa diferença de desempenho se deve à maneira como o sistema operacional gerencia os threads. A JVM oculta toda a diferença do SO. Existem muitos sites onde você pode ler sobre isso, como este , por exemplo. Mas isso não significa que a diferença desapareça.

Suponho que você esteja executando o Java 8+ JVM. Devido a esse fato, sugiro que você tente usar os recursos de programação funcional e de fluxo. A programação funcional é muito útil quando você tem muitos pequenos problemas independentes e deseja alternar facilmente da execução sequencial para a paralela. A boa notícia é que você não precisa definir uma política para determinar quantos threads você precisa gerenciar (como no ExecutorService). Apenas por exemplo (extraído daqui ):

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

}

Resultado:

Para fluxos normais, leva 1 minuto e 10 segundos. Para fluxos paralelos, leva 23 segundos. PS testado com i7-7700, 16G RAM, Windows 10

Então, sugiro que você leia sobre programação de funções, fluxo, função lambda em Java e tente implementar um pequeno número de teste com seu código (adaptado para funcionar neste novo contexto).

xcesco
fonte
Uso fluxos em outras partes do software, mas, neste caso, as tarefas são criadas ao percorrer um gráfico. Eu não saberia como quebrar isso usando fluxos.
Nils
Você pode percorrer o gráfico, criar uma lista e depois usar fluxos?
Xcesco
Fluxos paralelos são apenas açúcar sintático para um ForkJoinPool. Que eu tentei (veja @KarolDowbecki comentário acima).
Nils
0

Você poderia postar as estatísticas do sistema? O gerenciador de tarefas é bom o suficiente para fornecer alguma pista se essa é a única ferramenta disponível. É fácil saber se suas tarefas estão aguardando IO - que soa como o culpado com base no que você descreveu. Pode ser devido a um problema de gerenciamento de memória ou a biblioteca pode gravar alguns dados temporários no disco, etc.

Quando você diz 25% da utilização da CPU, você quer dizer que apenas alguns núcleos estão ocupados trabalhando ao mesmo tempo? (Pode ser que todos os núcleos funcionem de tempos em tempos, mas não simultaneamente.) Você verifica quantos threads (ou processos) são realmente criados no sistema? O número é sempre maior que o número de núcleos?

Se houver threads suficientes, muitos deles estão ociosos aguardando alguma coisa? Se verdadeiro, você pode tentar interromper (ou anexar um depurador) para ver o que eles estão esperando.

Xiao-Feng Li
fonte
Eu adicionei uma captura de tela do gerenciador de tarefas para uma execução que representa esse problema. O próprio aplicativo cria tantos threads quanto núcleos físicos na máquina. Java contribui com pouco mais de 50 threads para essa figura. Como já foi dito, o VisualVM diz que todos os threads estão ocupados (verde). Eles simplesmente não levam a CPU ao limite no Windows. Eles fazem no Linux.
Nils
@ Nils Eu suspeito que você realmente não tem todos os tópicos ocupados ao mesmo tempo, mas na verdade apenas 9 a 10 deles. Eles são agendados aleatoriamente em todos os núcleos; portanto, você tem em média 9/44 = 20% de utilização. Você pode usar os encadeamentos Java diretamente em vez do ExecutorService para ver a diferença? Não é difícil criar 44 threads, e cada um obtém o Runnable / Callable de um pool de tarefas / fila. (Embora mostra VisualVM os tópicos Java estão ocupados, a realidade pode ser que os 44 tópicos estão programadas rapidamente, de modo que todos eles a chance de correr no período de amostragem de VisualVM.)
Xiao-Feng Li
Esse é um pensamento e algo que eu realmente fiz em algum momento. Na minha implementação, também assegurei que o acesso nativo fosse local para cada thread, mas isso não fez nenhuma diferença.
Nils