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:
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?
ForkJoinPool
vez deExecutorService
? A utilização de 25% da CPU é muito baixa se o problema estiver ligado à CPU.ForkJoinPool
é mais eficiente que o agendamento manual.Respostas:
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 .
fonte
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
fonte
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 ):
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).
fonte
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.
fonte