Nosso professor de ciência da computação disse uma vez que, por algum motivo, é mais eficiente fazer uma contagem regressiva do que contar. Por exemplo, se você precisar usar um loop FOR e o índice do loop não for usado em algum lugar (como imprimir uma linha de N * na tela), quero dizer esse código assim:
for (i = N; i >= 0; i--)
putchar('*');
é melhor que:
for (i = 0; i < N; i++)
putchar('*');
É mesmo verdade? E se sim, alguém sabe o porquê?
c
performance
loops
Prumo
fonte
fonte
putchar
está usando 99,9999% do tempo (mais ou menos).i
não está assinado, o primeiro loop é infinito?Respostas:
Antigamente, quando os computadores ainda eram retirados à mão de sílica fundida, quando microcontroladores de 8 bits percorriam a Terra, e quando seu professor era jovem (ou o professor de seu professor era jovem), havia uma instrução comum de máquina chamada decrement and skip se zero (DSZ). Os programadores de montagem de Hotshot usaram esta instrução para implementar loops. Máquinas posteriores receberam instruções mais sofisticadas, mas ainda havia alguns processadores nos quais era mais barato comparar algo com zero do que comparar com qualquer outra coisa. (É verdade mesmo em algumas máquinas RISC modernas, como PPC ou SPARC, que reservam um registro inteiro para ser sempre zero.)
Então, se você montar seus loops para comparar com zero em vez de
N
, o que pode acontecer?É provável que essas diferenças resultem em alguma melhoria mensurável em programas reais em um processador fora de ordem moderno? Altamente improvável. Na verdade, eu ficaria impressionado se você pudesse mostrar uma melhoria mensurável, mesmo em uma marca de microbench.
Resumo: Eu bato na cabeça do seu professor! Você não deve aprender pseudo-fatos obsoletos sobre como organizar loops. Você deve aprender que o mais importante sobre os loops é garantir que eles terminem , produzam respostas corretas e sejam fáceis de ler . Eu gostaria que seu professor se concentrasse em coisas importantes e não na mitologia.
fonte
putchar
leva muitas ordens de magnitude a mais do que a sobrecarga do loop.j=N-i
mostra que os dois loops são equivalentes.Aqui está o que pode acontecer em algum hardware, dependendo do que o compilador pode deduzir sobre o intervalo dos números que você está usando: com o loop de incremento, você deve testar
i<N
cada vez que o loop for executado. Para a versão decrescente, o sinalizador de transporte (definido como efeito colateral da subtração) pode informar automaticamente sei>=0
. Isso economiza um teste por vez em todo o ciclo.Na realidade, no hardware moderno do processador em pipeline, esse material é quase certamente irrelevante, pois não há um mapeamento simples de 1-1 das instruções para os ciclos de clock. (Embora eu pudesse imaginar isso surgindo se você estivesse fazendo coisas como gerar sinais de vídeo precisamente cronometrados a partir de um microcontrolador. Mas, de qualquer maneira, você escreveria em linguagem assembly.)
fonte
No conjunto de instruções Intel x86, criar um loop para contar até zero geralmente pode ser feito com menos instruções do que um loop que conta até uma condição de saída diferente de zero. Especificamente, o registro ECX é tradicionalmente usado como um contador de loop em x86 asm, e o conjunto de instruções Intel possui uma instrução jcxz jump especial que testa o registro ECX quanto a zero e salta com base no resultado do teste.
No entanto, a diferença de desempenho será insignificante, a menos que seu loop já seja muito sensível às contagens do ciclo do relógio. Contar até zero pode reduzir 4 ou 5 ciclos de relógio a cada iteração do loop em comparação com a contagem, por isso é realmente mais uma novidade do que uma técnica útil.
Além disso, hoje em dia, um bom compilador de otimização deve poder converter seu código-fonte de loop de contagem regressiva em código de máquina zero (dependendo de como você usa a variável de índice de loop), para que realmente não haja motivo para escrever seus loops em maneiras estranhas apenas para espremer um ciclo ou dois aqui e ali.
fonte
Sim..!!
Contar de N até 0 é um pouco mais rápido que Contar de 0 a N no sentido de como o hardware lidará com a comparação.
Observe a comparação em cada loop
A maioria dos processadores tem comparação com instrução zero ... então o primeiro será traduzido para o código da máquina como:
Mas o segundo precisa carregar N da memória toda vez
Portanto, não é por causa da contagem regressiva ou alta .. Mas por causa de como seu código será traduzido em código de máquina ..
Portanto, contar de 10 a 100 é o mesmo que contar de 100 a 10,
mas contar de i = 100 a 0 é mais rápido que de i = 0 a 100 - na maioria dos casos
E contar de i = N a 0 é mais rápido que de i = 0 a N
fonte
Em C para psudo-montagem:
torna-se em
enquanto:
torna-se em
Observe a falta de comparação na segunda psudo-montagem. Em muitas arquiteturas, existem sinalizadores definidos por operações aritmáticas (adicionar, subtrair, multiplicar, dividir, incrementar, diminuir) que você pode usar para saltos. Isso geralmente fornece o que é essencialmente uma comparação do resultado da operação com 0 de graça. De fato, em muitas arquiteturas
é semanticamente o mesmo que
Além disso, a comparação com um 10 no meu exemplo pode resultar em um código pior. 10 pode ter que viver em um registro, portanto, se houver escassez de custos e resultar em código extra para movimentar as coisas ou recarregar os 10 todas as vezes através do loop.
Às vezes, os compiladores podem reorganizar o código para tirar proveito disso, mas geralmente é difícil porque geralmente não conseguem ter certeza de que a inversão da direção através do loop é semanticamente equivalente.
fonte
i
não seja usado dentro do loop, obviamente você pode inverter isso, não é?Contagem regressiva mais rápida em casos como este:
porque
someObject.getAllObjects.size()
executa uma vez no começo.Certamente, um comportamento semelhante pode ser alcançado chamando
size()
fora do loop, como Peter mencionou:fonte
exec
.Talvez. Mas, em mais de 99% do tempo, isso não importa, então você deve usar o teste mais "sensato" para terminar o loop e, por sensato, quero dizer que é preciso a menor quantidade de pensamento de um leitor para descobrir o que o loop está fazendo (incluindo o que o faz parar). Faça seu código corresponder ao modelo mental (ou documentado) do que o código está fazendo.
Se o loop estiver funcionando no caminho através de uma matriz (ou lista, ou qualquer outra coisa), um contador de incremento geralmente corresponderá melhor com o modo como o leitor pode estar pensando no que o loop está fazendo - codifique seu loop dessa maneira.
Mas se você estiver trabalhando em um contêiner que possui
N
itens e estiver removendo os itens à medida que avança, poderá fazer mais sentido cognitivo trabalhar no balcão.Um pouco mais detalhadamente sobre o 'talvez' na resposta:
É verdade que, na maioria das arquiteturas, o teste de um cálculo que resulta em zero (ou passa de zero a negativo) não requer instruções explícitas de teste - o resultado pode ser verificado diretamente. Se você deseja testar se um cálculo resulta em algum outro número, o fluxo de instruções geralmente precisará ter uma instrução explícita para testar esse valor. No entanto, especialmente com CPUs modernas, esse teste geralmente adiciona menos tempo adicional ao nível do ruído a uma construção em loop. Especialmente se esse loop estiver executando E / S.
Por outro lado, se você fizer uma contagem regressiva de zero e usar o contador como um índice de matriz, por exemplo, poderá encontrar o código funcionando contra a arquitetura de memória do sistema - as leituras de memória geralmente fazem com que um cache 'olhe para frente' vários locais de memória além do atual em antecipação a uma leitura seqüencial. Se você estiver trabalhando de trás para frente na memória, o sistema de armazenamento em cache pode não antecipar leituras de um local de memória em um endereço de memória mais baixo. Nesse caso, é possível que fazer um loop para trás prejudicar o desempenho. No entanto, eu provavelmente codificaria o loop dessa maneira (desde que o desempenho não se tornasse um problema) porque a correção é fundamental e fazer o código corresponder a um modelo é uma ótima maneira de ajudar a garantir a correção. O código incorreto é o mais otimizado possível.
Então, eu tenderia a esquecer o conselho do professor (é claro, não no teste dele - você ainda deve ser pragmático no que diz respeito à sala de aula), a menos e até que o desempenho do código realmente importe.
fonte
Em algumas CPUs mais antigas, existem / houve instruções como
DJNZ
== "decrementar e pular se não for zero". Isso permitia loops eficientes nos quais você carregava um valor inicial de contagem em um registrador e, em seguida, era possível gerenciar efetivamente um loop decrescente com uma instrução. No entanto, estamos falando de ISAs dos anos 80 aqui - seu professor está seriamente fora de contato se ele acha que essa "regra de ouro" ainda se aplica às CPUs modernas.fonte
Prumo,
Não até você realizar microoptimizações; nesse momento, você terá o manual da sua CPU em mãos. Além disso, se você estivesse fazendo esse tipo de coisa, provavelmente não precisaria fazer essa pergunta de qualquer maneira. :-) Mas, evidentemente, seu professor não se inscreve nessa idéia ...
Há quatro coisas a considerar em seu exemplo de loop:
A comparação é (como outros indicaram) relevante para arquiteturas de processador específicas . Existem mais tipos de processadores do que aqueles que executam o Windows. Em particular, pode haver uma instrução que simplifique e acelere as comparações com 0.
Em alguns casos, é mais rápido ajustar para cima ou para baixo. Normalmente, um bom compilador irá descobrir e refazer o loop, se puder. Nem todos os compiladores são bons.
Você está acessando um syscall com putchar. Isso é massivamente lento. Além disso, você está renderizando na tela (indiretamente). Isso é ainda mais lento. Pense na proporção de 1000: 1 ou mais. Nesta situação, o corpo do loop supera totalmente e totalmente o custo do ajuste / comparação do loop.
Um layout de cache e memória pode ter um grande efeito no desempenho. Nesta situação, isso não importa. No entanto, se você estivesse acessando uma matriz e precisasse de um desempenho ideal, caberia a você investigar como o compilador e o processador distribuem a memória acessa e ajustar o software para aproveitar ao máximo isso. O exemplo de estoque é o dado em relação à multiplicação de matrizes.
fonte
O que importa muito mais do que aumentar ou diminuir o contador é aumentar ou diminuir a memória. A maioria dos caches é otimizada para aumentar a memória, não a memória inativa. Como o tempo de acesso à memória é o gargalo enfrentado pela maioria dos programas atualmente, isso significa que alterar o programa para aumentar a memória pode resultar em um aumento no desempenho, mesmo que isso exija a comparação do contador com um valor diferente de zero. Em alguns dos meus programas, vi uma melhoria significativa no desempenho alterando meu código para aumentar a memória em vez de diminuí-lo.
Cético? Basta escrever um programa para cronometrar loops subindo / descendo memória. Aqui está a saída que eu tenho:
(em que "mus" significa microssegundos) da execução deste programa:
Ambos
sum_abs_up
esum_abs_down
fazem a mesma coisa (soma o vetor de números) e são cronometrados da mesma maneira, com a única diferença quesum_abs_up
aumenta a memória esum_abs_down
diminui a memória. Eu passo atévec
por referência para que ambas as funções acessem os mesmos locais de memória. No entanto,sum_abs_up
é consistentemente mais rápido quesum_abs_down
. Faça uma corrida você mesmo (eu compilei com g ++ -O3).É importante observar o quão apertado é o tempo que estou fazendo. Se o corpo de um loop for grande, provavelmente não importará se o iterador aumenta ou diminui a memória, pois o tempo que leva para executar o corpo do loop provavelmente dominará completamente. Além disso, é importante mencionar que, com alguns loops raros, diminuir a memória às vezes é mais rápido do que subir. Mas mesmo com tais laços nunca foi o caso que vai a memória foi sempre mais lento do que ir para baixo (ao contrário de loops de pequenos-bodied que sobem memória, para que o oposto é freqüentemente verdade, na verdade, para um pequeno punhado de loops I' cronometrado, o aumento no desempenho subindo a memória foi de 40 +%).
O ponto é, como regra geral, se você tem a opção, se o corpo do loop é pequeno e se há pouca diferença entre fazer com que o loop suba a memória em vez de diminuí-lo, você deve subir a memória.
A FYI
vec_original
existe para a experimentação, para facilitar a mudançasum_abs_up
esum_abs_down
de uma maneira que as alterevec
, sem permitir que essas mudanças afetem os horários futuros. Eu recomendo a brincar comsum_abs_up
esum_abs_down
e cronometrando os resultados.fonte
independentemente da direção, sempre use o formato de prefixo (++ i em vez de i ++)!
ou
Explicação: http://www.eskimo.com/~scs/cclass/notes/sx7b.html
Além disso, você pode escrever
Mas eu esperaria que os compiladores modernos sejam capazes de fazer exatamente essas otimizações.
fonte
É uma pergunta interessante, mas, na prática, não acho importante e não torna um loop melhor que o outro.
De acordo com esta página da Wikipedia: Leap second , "... o dia solar se torna 1,7 ms a mais a cada século devido principalmente ao atrito das marés". Mas se você está contando dias até o seu aniversário, você realmente se importa com essa pequena diferença de tempo?
É mais importante que o código fonte seja fácil de ler e entender. Esses dois loops são um bom exemplo de por que a legibilidade é importante - eles não repetem o mesmo número de vezes.
Eu apostaria que a maioria dos programadores lê (i = 0; i <N; i ++) e entende imediatamente que isso faz um loop N vezes. Um loop de (i = 1; i <= N; i ++), para mim de qualquer maneira, é um pouco menos claro, e com (i = N; i> 0; i--) eu tenho que pensar nisso por um momento . É melhor se a intenção do código for diretamente para o cérebro sem que seja necessário pensar.
fonte
Estranhamente, parece que há uma diferença. Pelo menos em PHP. Considere a seguinte referência:
Os resultados são interessantes:
Se alguém souber o porquê, seria bom saber :)
EDIT : Os resultados são os mesmos, mesmo se você começar a contar não a partir de 0, mas outro valor arbitrário. Portanto, provavelmente não há apenas comparação com zero, o que faz a diferença?
fonte
Ele pode ser mais rápido.
No processador NIOS II com o qual estou trabalhando atualmente, o tradicional loop for
produz a montagem:
Se contarmos
temos uma montagem que precisa de 2 instruções a menos.
Se tivermos loops aninhados, onde o loop interno é executado muito, podemos ter uma diferença mensurável:
Se o loop interno for escrito como acima, o tempo de execução é: 0,12199999999999999734 segundos. Se o loop interno for gravado da maneira tradicional, o tempo de execução será: 0,117199999999999998623 segundos. Portanto, a contagem decrescente do loop é cerca de 30% mais rápida.
Mas: esse teste foi feito com todas as otimizações do GCC desativadas. Se ativá-los, o compilador é realmente mais inteligente que essa otimização manual e ainda mantém o valor em um registro durante todo o loop e obteríamos um assembly como
Neste exemplo em particular o compilador nem percebe, essa variável um sempre será 1 após a execução do loop e ignora todos os loops.
No entanto, experimentei que, às vezes, se o corpo do loop é complexo o suficiente, o compilador não é capaz de fazer essa otimização; portanto, a maneira mais segura de obter sempre uma execução rápida do loop é escrever:
É claro que isso só funciona, se não importa que o loop seja executado em sentido inverso e, como Betamoo disse, apenas se você estiver contando até zero.
fonte
O que seu professor disse foi uma declaração oblíqua, sem muitos esclarecimentos. NÃO é que o decremento seja mais rápido que o incremento, mas você pode criar um loop muito mais rápido com o decremento do que com o incremento.
Sem falar muito sobre isso, sem a necessidade de usar o contador de loop, etc - o que importa abaixo é apenas a velocidade e a contagem de loop (diferente de zero).
Aqui está como a maioria das pessoas implementa loop com 10 iterações:
Para 99% dos casos, é tudo o que precisamos, mas junto com PHP, PYTHON, JavaScript, existe todo o mundo de software crítico de tempo (geralmente incorporado, SO, jogos, etc.) em que os tiques de CPU realmente importam, então veja brevemente o código de montagem de:
após a compilação (sem otimização), a versão compilada pode ser assim (VS2015):
O loop inteiro é de 8 instruções (26 bytes). Nele - na verdade existem 6 instruções (17 bytes) com 2 ramificações. Sim, sim, eu sei que isso pode ser feito melhor (é apenas um exemplo).
Agora considere essa construção frequente que você encontrará com frequência por escrito pelo desenvolvedor incorporado:
Ele também itera 10 vezes (sim, eu sei que o valor é diferente em comparação com o loop for mostrado, mas nos preocupamos com a contagem de iterações aqui). Isso pode ser compilado para isso:
5 instruções (18 bytes) e apenas um ramo. Na verdade, existem 4 instruções no loop (11 bytes).
O melhor é que algumas CPUs (compatíveis com x86 / x64) possuem instruções que podem diminuir um registro, comparar o resultado com zero e executar ramificações se o resultado for diferente de zero. Praticamente todos os cpus de PC implementam esta instrução. Utilizando-o, o loop é na verdade apenas uma (sim uma) instrução de 2 bytes:
Eu tenho que explicar o que é mais rápido?
Agora, mesmo que uma CPU específica não implemente a instrução acima, tudo o que é necessário para emular é um decremento seguido de salto condicional se o resultado da instrução anterior for zero.
Portanto, independentemente de alguns casos que você possa apontar como comentário, por que eu estou errado, etc, etc. EU SALIENTO - SIM É BENEFICIAL FAZER LOOP DOWNWARDS se você souber como, por que e quando.
PS. Sim, eu sei que o compilador inteligente (com nível de otimização apropriado) reescreverá o loop (com contador de loop ascendente) em do..time equivalente a iterações constantes do loop ... (ou desenrolá-lo) ...
fonte
Não, isso não é verdade. Uma situação em que poderia ser mais rápido é quando você chamaria uma função para verificar os limites durante cada iteração de um loop.
Mas se for menos claro fazê-lo dessa maneira, não vale a pena. Em idiomas modernos, você deve usar um loop foreach sempre que possível. Você mencionou especificamente o caso em que deve usar um loop foreach - quando não precisa do índice.
fonte
for(int i=0, siz=myCollection.size(); i<siz; i++)
.O ponto é que, ao fazer uma contagem regressiva, você não precisa verificar
i >= 0
separadamente para diminuiri
. Observar:A comparação e o decremento
i
podem ser feitos em uma expressão.Veja outras respostas sobre por que isso se resume a menos instruções x86.
Quanto a fazer uma diferença significativa em sua aplicação, acho que depende de quantos loops você possui e de quão profundamente aninhados eles são. Mas para mim, é tão legível fazê-lo dessa maneira, então eu faço assim mesmo.
fonte
Agora, acho que você já teve várias palestras de montagem :) Gostaria de apresentar outro motivo para a abordagem de cima para baixo.
A razão para ir de cima é muito simples. No corpo do loop, você pode alterar acidentalmente o limite, o que pode resultar em comportamento incorreto ou mesmo em loop sem fim.
Veja esta pequena parte do código Java (a linguagem não importa, acho que por esse motivo):
Portanto, o que quero dizer é que você deve considerar preferir ir de cima para baixo ou ter uma constante como limite.
fonte
for (int i=0; i < 999; i++) {
.for(int xa=0; xa<collection.size(); xa++) { collection.add(SomeObject); ... }
Em um nível de montador, um loop que conta até zero é geralmente um pouco mais rápido do que aquele que conta até um determinado valor. Se o resultado de um cálculo for igual a zero, a maioria dos processadores definirá um sinalizador zero. Se subtrair um faz um cálculo em torno de zero passado, isso normalmente altera o sinalizador de transporte (em alguns processadores ele define em outros, o apaga), então a comparação com zero é essencialmente gratuita.
Isso é ainda mais verdadeiro quando o número de iterações não é uma constante, mas uma variável.
Em casos triviais, o compilador pode otimizar a direção da contagem de um loop automaticamente, mas em casos mais complexos, pode ser que o programador saiba que a direção do loop é irrelevante para o comportamento geral, mas o compilador não pode provar isso.
fonte