Eu sou novo em java, e estava executando algum código ontem à noite, e isso realmente me incomodou. Eu estava construindo um programa simples para exibir todas as saídas X em um loop for, e notei uma enorme queda no desempenho quando usei o módulo como variable % variable
vs variable % 5000
ou outros enfeites. Alguém pode me explicar por que isso é e o que está causando isso? Para que eu possa ser melhor ...
Aqui está o código "eficiente" (desculpe se entendi um pouco de sintaxe, não estou no computador com o código no momento)
long startNum = 0;
long stopNum = 1000000000L;
for (long i = startNum; i <= stopNum; i++){
if (i % 50000 == 0) {
System.out.println(i);
}
}
Aqui está o "código ineficiente"
long startNum = 0;
long stopNum = 1000000000L;
long progressCheck = 50000;
for (long i = startNum; i <= stopNum; i++){
if (i % progressCheck == 0) {
System.out.println(i);
}
}
Lembre-se de que eu tinha uma variável de data para medir as diferenças e, uma vez que ficou longa o suficiente, a primeira levou 50ms enquanto a outra levou 12 segundos ou algo assim. Você pode ter que aumentar stopNum
ou diminuir progressCheck
se o seu PC for mais eficiente que o meu ou não.
Procurei essa pergunta na Web, mas não consigo encontrar uma resposta, talvez não esteja fazendo a pergunta certa.
EDIT: Eu não esperava que minha pergunta fosse tão popular, agradeço todas as respostas. Realizei uma referência em cada metade do tempo gasto, e o código ineficiente demorou consideravelmente mais, 1/4 de segundo versus 10 segundos mais ou menos. É verdade que eles estão usando println, mas ambos estão fazendo a mesma quantidade, então eu não imaginaria que isso distorceria muito, especialmente porque a discrepância é repetível. Quanto às respostas, como sou novo em Java, deixarei os votos decidirem por enquanto qual é a melhor. Vou tentar escolher um até quarta-feira.
EDIT2: Vou fazer outro teste hoje à noite, onde, em vez de módulo, ele apenas incrementa uma variável e, quando atinge progressCheck, ele executa uma e, em seguida, redefine a variável para 0. para uma terceira opção.
EDIT3.5:
Eu usei esse código e abaixo mostrarei meus resultados .. Obrigado a todos pela maravilhosa ajuda! Eu também tentei comparar o valor curto do longo com 0, então todas as minhas novas verificações acontecem sempre "65536" vezes, tornando-o igual em repetições.
public class Main {
public static void main(String[] args) {
long startNum = 0;
long stopNum = 1000000000L;
long progressCheck = 65536;
final long finalProgressCheck = 50000;
long date;
// using a fixed value
date = System.currentTimeMillis();
for (long i = startNum; i <= stopNum; i++) {
if (i % 65536 == 0) {
System.out.println(i);
}
}
long final1 = System.currentTimeMillis() - date;
date = System.currentTimeMillis();
//using a variable
for (long i = startNum; i <= stopNum; i++) {
if (i % progressCheck == 0) {
System.out.println(i);
}
}
long final2 = System.currentTimeMillis() - date;
date = System.currentTimeMillis();
// using a final declared variable
for (long i = startNum; i <= stopNum; i++) {
if (i % finalProgressCheck == 0) {
System.out.println(i);
}
}
long final3 = System.currentTimeMillis() - date;
date = System.currentTimeMillis();
// using increments to determine progressCheck
int increment = 0;
for (long i = startNum; i <= stopNum; i++) {
if (increment == 65536) {
System.out.println(i);
increment = 0;
}
increment++;
}
//using a short conversion
long final4 = System.currentTimeMillis() - date;
date = System.currentTimeMillis();
for (long i = startNum; i <= stopNum; i++) {
if ((short)i == 0) {
System.out.println(i);
}
}
long final5 = System.currentTimeMillis() - date;
System.out.println(
"\nfixed = " + final1 + " ms " + "\nvariable = " + final2 + " ms " + "\nfinal variable = " + final3 + " ms " + "\nincrement = " + final4 + " ms" + "\nShort Conversion = " + final5 + " ms");
}
}
Resultados:
- fixo = 874 ms (normalmente em torno de 1000ms, mas mais rápido devido a uma potência de 2)
- variável = 8590 ms
- variável final = 1944 ms (era ~ 1000ms ao usar 50000)
- incremento = 1904 ms
- Conversão curta = 679 ms
Não surpreende o suficiente, devido à falta de divisão, a conversão curta foi 23% mais rápida que a maneira "rápida". Isso é interessante notar. Se você precisar mostrar ou comparar algo a cada 256 vezes (ou por aí), faça isso e use
if ((byte)integer == 0) {'Perform progress check code here'}
UMA NOTA INTERESSANTE FINAL, o uso do módulo na "Variável declarada final" com 65536 (número não muito bonito) foi metade da velocidade (mais lenta) do que o valor fixo. Onde antes estava comparando com a mesma velocidade.
fonte
final
na frente daprogressCheck
variável, ambos correm na mesma velocidade novamente. Isso me leva a acreditar que o compilador ou o JIT consegue otimizar o loop quando sabe queprogressCheck
é constante.Respostas:
Você está medindo o stub OSR (substituição na pilha) .
O OSR stub é uma versão especial do método compilado, projetado especificamente para transferir a execução do modo interpretado para o código compilado enquanto o método está em execução.
Os stubs OSR não são tão otimizados quanto os métodos regulares, porque precisam de um layout de quadro compatível com o quadro interpretado. Eu já mostrei isso nas seguintes respostas: 1 , 2 , 3 .
Uma coisa semelhante acontece aqui também. Enquanto o "código ineficiente" está executando um loop longo, o método é compilado especialmente para a substituição na pilha dentro do loop. O estado é transferido do quadro interpretado para o método compilado por OSR e esse estado inclui
progressCheck
a variável local. Nesse ponto, o JIT não pode substituir a variável pela constante e, portanto, não pode aplicar certas otimizações, como redução de força .Em particular, isso significa que o JIT não substitui a divisão inteira pela multiplicação . (Veja Por que o uso GCC multiplicação por um número estranho na implementação divisão inteira? Para o truque asm de um compilador antes-do-tempo, quando o valor é uma constante de tempo de compilação após inlining / constant-propagação, se essas otimizações são habilitados Um literal inteiro inteiro na
%
expressão também é otimizadogcc -O0
, semelhante a aqui onde é otimizado pelo JITer, mesmo em um stub OSR.)No entanto, se você executar o mesmo método várias vezes, a segunda e a subsequente executarão o código regular (não OSR), que é totalmente otimizado. Aqui está uma referência para provar a teoria ( comparada usando JMH ):
E os resultados:
A primeira iteração de
divVar
é realmente muito mais lenta, devido ao stub OSR ineficientemente compilado. Porém, assim que o método é executado novamente desde o início, a nova versão irrestrita é executada, o que aproveita todas as otimizações disponíveis do compilador.fonte
System.out.println
quase necessariamente produzirá resultados de lixo, e o fato de que ambas as versões são igualmente rápidas não tem nada a ver com a OSR em particular , tanto quanto eu sei.1
é um pouco dúbio - o loop vazio também pode ser completamente otimizado. O segundo é mais parecido com esse. Mas, novamente, não está claro por que você atribui a diferença ao OSR especificamente . Eu diria: em algum momento, o método é JIT e se torna mais rápido. No meu entender, o OSR apenas faz com que o uso do código final otimizado seja (aproximadamente) "adiado para a próxima passagem de otimização". (continuação ...)%
operação teria o mesmo peso, pois uma execução otimizada só é possível, bem, se um otimizador fez um trabalho real. Portanto, o fato de uma variante de loop ser significativamente mais rápida que a outra prova a presença de um otimizador e prova ainda que ele não conseguiu otimizar um dos loops no mesmo grau que o outro (dentro do mesmo método!). Como essa resposta prova a capacidade de otimizar os dois loops no mesmo grau, deve haver algo que atrapalhou a otimização. E isso é OSR em 99,9% de todos os casos-XX:+PrintCompilation -XX:+TraceNMethodInstalls
.No seguimento do comentário @phuclv , verifiquei o código gerado pelo JIT 1 , os resultados são os seguintes:
para
variable % 5000
(divisão por constante):para
variable % variable
:Como a divisão sempre leva mais tempo que a multiplicação, o último trecho de código tem menos desempenho.
Versão Java:
1 - Opções de VM usadas:
-XX:+UnlockDiagnosticVMOptions -XX:CompileCommand=print,src/java/Main.main
fonte
imul
é 3 ciclos,idiv
está entre 30 e 90 ciclos. Portanto, a divisão inteira é entre 10x e 30x mais lenta que a multiplicação inteira.Como outros observaram, a operação geral do módulo exige que uma divisão seja feita. Em alguns casos, a divisão pode ser substituída (pelo compilador) por uma multiplicação. Mas ambos podem ser lentos em comparação com adição / subtração. Portanto, o melhor desempenho pode ser esperado por algo nesse sentido:
(Como uma pequena tentativa de otimização, usamos um contador de pré-decréscimo aqui, porque em muitas arquiteturas, comparando
0
imediatamente após uma operação aritmética, custa exatamente 0 instruções / ciclos de CPU, porque os sinalizadores da ALU já estão configurados adequadamente pela operação anterior. O compilador, no entanto, fará essa otimização automaticamente, mesmo se você escreverif (counter++ == 50000) { ... counter = 0; }
.)Observe que muitas vezes você realmente não quer / precisa de módulo, porque você sabe que seu contador de loop (
i
) ou o que quer que seja apenas incrementado em 1, e você realmente não se importa com o restante do módulo, basta ver se o contador de incremento por um atingir algum valor.Outro 'truque' é usar dois valores / limites de potência, por exemplo
progressCheck = 1024;
. O módulo de potência de dois pode ser calculado rapidamente via bit a bitand
, ou sejaif ( (i & (1024-1)) == 0 ) {...}
. Isso também deve ser muito rápido e, em algumas arquiteturas, pode superar o explícitocounter
acima.fonte
if()
corpo se torna um corpo de loop externo, e o material externo aoif()
torna-se um corpo de loop interno que executamin(progressCheck, stopNum-i)
iterações. Portanto, no início, e sempre quecounter
chegar a 0, vocêlong next_stop = i + min(progressCheck, stopNum-i);
deve configurar umfor(; i< next_stop; i++) {}
loop. Nesse caso, o loop interno está vazio e, com sorte, deve otimizar completamente, você pode fazer isso na fonte e facilitar o JITer, reduzindo o loop para i + = 50k.--counter
é tão rápido quanto a minha versão de incremento, mas menos code.also era 1 inferior ao que deveria ser, eu sou curioso se ele deve sercounter--
para obter o número exato que você quer , não que seja muita diferença--counter
está desativado por um.counter--
fornecerá exatamente oprogressCheck
número de iterações (ou você pode definir, éprogressCheck = 50001;
claro).Também estou surpreso ao ver o desempenho dos códigos acima. É tudo sobre o tempo gasto pelo compilador para executar o programa de acordo com a variável declarada. No segundo exemplo (ineficiente):
Você está executando a operação do módulo entre duas variáveis. Aqui, o compilador deve verificar o valor
stopNum
eprogressCheck
acessar o bloco de memória específico localizado para essas variáveis todas as vezes após cada iteração, porque é uma variável e seu valor pode ser alterado.É por isso que após cada compilador de iteração foi para o local da memória para verificar o valor mais recente das variáveis. Portanto, no momento da compilação, o compilador não conseguiu criar um código de bytes eficiente.
No primeiro exemplo de código, você está executando um operador de módulo entre uma variável e um valor numérico constante que não será alterado na execução e no compilador, não sendo necessário verificar o valor desse valor numérico a partir da localização da memória. É por isso que o compilador conseguiu criar código de bytes eficiente. Se você declarar
progressCheck
como uma variávelfinal
ou comofinal static
variável, no momento do compilador em tempo de execução / tempo de compilação saiba que é uma variável final e seu valor não será alterado, substitua-oprogressCheck
por50000
no código:Agora você pode ver que esse código também se parece com o primeiro exemplo de código (eficiente). O desempenho do primeiro código e, como mencionamos acima, ambos os códigos funcionarão eficientemente. Não haverá muita diferença no tempo de execução dos dois exemplos de código.
fonte
volatile
o 'compilador' não lerá seu valor da RAM repetidamente.