É mais rápido fazer uma contagem regressiva do que contar?

Nosso professor de ciências 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 de loop não for usado em algum lugar (como imprimir uma linha de N * na canvas), quero dizer que o código é assim:

for (i = N; i >= 0; i--) putchar('*'); 

é melhor que:

 for (i = 0; i < N; i++) putchar('*'); 

Isso é realmente verdade? E se assim for, alguém sabe por quê?

Isso é realmente verdade? e se é que alguém sabe por quê?

Antigamente, quando os computadores ainda eram lascados à mão com sílica fundida, quando os microcontroladores de 8 bits vagavam pela Terra, e quando seu professor era jovem (ou o professor de seu professor era jovem), havia uma instrução de máquina comum chamada decremento e pular se zero (DSZ). Os programadores de assembly do Hotshot usaram essa instrução para implementar loops. Máquinas posteriores receberam instruções mais sofisticadas, mas ainda havia muito poucos 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ê manipular seus loops para comparar com zero em vez de N , o que pode acontecer?

  • Você pode salvar um registro
  • Você pode obter uma instrução de comparação com uma codificação binária menor
  • Se uma instrução anterior configurar um sinalizador (provavelmente apenas em máquinas da família x86), talvez você nem precise de uma instrução de comparação explícita

Essas diferenças provavelmente resultarão em alguma melhoria mensurável em programas reais em um processador moderno fora de ordem? Altamente improvável. Na verdade, ficaria impressionado se você pudesse mostrar uma melhoria mensurável mesmo em um microbenchmark.

Resumo: Eu bato seu professor na cabeça! Você não deveria estar aprendendo pseudo-fatos obsoletos sobre como organizar loops. Você deve estar aprendendo que a coisa mais importante sobre os loops é ter certeza de que eles terminam , produzem respostas corretas e são fáceis de ler . Eu gostaria que seu professor se concentrasse nas coisas importantes e não na mitologia.

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ê tem que testar i cada volta do loop. Para a versão decrementante, o flag carry (definido como um efeito colateral da subtração) pode informar automaticamente se i>=0 . Isso salva um teste por vez em volta do loop.

Na realidade, em hardware de processador com pipelines moderno, esse material é quase certamente irrelevante, pois não há um mapeamento simples de 1-1 de instruções para ciclos de clock. (Embora eu pudesse imaginá-lo surgindo se você estivesse fazendo coisas como gerar sinais de vídeo com tempo determinado a partir de um microcontrolador. Mas então você escreveria em linguagem assembly de qualquer maneira.)

No conjunto de instruções Intel x86, geralmente é possível criar um loop para fazer a contagem regressiva até zero com menos instruções do que um loop que conta até uma condição de saída diferente de zero. Especificamente, o registrador ECX é tradicionalmente usado como um contador de loop em x86 asm, e o conjunto de instruções Intel possui uma instrução especial de salto jcxz que testa o registrador ECX para 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 de clock. A contagem para zero pode reduzir 4 ou 5 ciclos de clock de cada iteração do loop em comparação com a contagem, por isso é mais uma novidade do que uma técnica útil.

Além disso, um bom compilador de otimização nos dias de hoje deve ser capaz de converter seu código fonte de contagem regressiva em contagem regressiva para código de máquina zero (dependendo de como você usa a variável de índice de loop), então não há motivo para escrever seus loops maneiras estranhas apenas para apertar um ciclo ou dois aqui e ali.

Sim..!!

A contagem de N até 0 é um pouco mais rápida do que a contagem de 0 a N, no sentido de como o hardware lidará com a comparação.

Observe a comparação em cada loop

 i>=0 i 

A maioria dos processadores tem comparação com a instrução zero ... para que o primeiro seja traduzido para código de máquina como:

  1. Carregar i
  2. Compare e salte se for menor que ou igual a zero

Mas o segundo precisa carregar a memory de forma N a cada vez

  1. carregar i
  2. carregar N
  3. Sub i e N
  4. Compare e salte se for menor que ou igual a zero

Então, não é por causa da contagem regressiva. Mas por causa de como seu código será traduzido em código de máquina.

Então, contando de 10 a 100 é o mesmo que contar 100 a 10
Mas contando 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

  • Note que hoje em dia os compiladores podem fazer essa otimização para você (se for inteligente o suficiente)
  • Note também que o gasoduto pode causar o efeito de anomalia de Belady (não pode ter certeza do que será melhor)
  • Finalmente: por favor, note que os 2 loops for apresentados não são equivalentes .. o primeiro imprime mais um * ....

Relacionado: Por que o n ++ executa mais rápido que n = n + 1?

Em C para psudo-assembly:

 for (i = 0; i < 10; i++) { foo(i); } 

torna-se em

  clear i top_of_loop: call foo increment i compare 10, i jump_less top_of_loop 

enquanto:

 for (i = 10; i >= 0; i--) { foo(i); } 

torna-se em

  load i, 10 top_of_loop: call foo decrement i jump_not_neg top_of_loop 

Observe a falta da comparação no segundo conjunto de psudo. Em muitas arquiteturas existem bandeiras que são definidas por operações aritmáticas (adicionar, subtrair, multiplicar, dividir, incrementar, decrementar) que você pode usar para saltos. Estes geralmente lhe dão o que é essencialmente uma comparação do resultado da operação com 0 de graça. De fato, em muitas arquiteturas

 x = x - 0 

é semanticamente o mesmo que

 compare x, 0 

Além disso, a comparação contra um 10 no meu exemplo poderia resultar em código pior. 10 pode ter que viver em um registro, por isso, se eles estão em falta que custa e pode resultar em código extra para mover as coisas ao redor ou recarregar os 10 a cada vez através do loop.

Às vezes, os compiladores podem reorganizar o código para tirar proveito disso, mas muitas vezes é difícil, porque muitas vezes não conseguem ter certeza de que reverter a direção pelo loop é semanticamente equivalente.

Contar mais rápido no caso como este:

 for (i = someObject.getAllObjects.size(); i >= 0; i--) {…} 

porque someObject.getAllObjects.size() executado uma vez no começo.


Claro, um comportamento semelhante pode ser alcançado chamando size() fora do loop, como Peter mencionou:

 size = someObject.getAllObjects.size(); for (i = 0; i < size; i++) {…} 

É mais rápido fazer uma contagem regressiva do que para cima?

Talvez. Mas, muito mais do que 99% do tempo, isso não importa, então você deve usar o teste mais “sensato” para encerrar o ciclo, e por sensatez, quero dizer que é preciso menos pensamento para um leitor 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 está funcionando, é através de um array (ou lista, ou o que for), um contador de incremento freqüentemente irá combinar 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 tenha N itens e estiver removendo os itens à medida que avança, pode fazer mais sentido cognitivo trabalhar o contador abaixo.

Um pouco mais detalhadamente sobre o ‘talvez’ na resposta:

É verdade que na maioria das arquiteturas, testar um cálculo que resulte em zero (ou passar de zero a negativo) não requer nenhuma instrução de teste explícita – o resultado pode ser verificado diretamente. Se você quiser testar se um cálculo resulta em algum outro número, o stream de instruções geralmente terá que ter uma instrução explícita para testar esse valor. No entanto, especialmente com CPUs modernas, esse teste geralmente adiciona tempo adicional menor que o nível de ruído a uma construção de loop. Particularmente se esse loop estiver executando E / S.

Por outro lado, se você contar abaixo de zero e usar o contador como um índice de matriz, por exemplo, você pode encontrar o código trabalhando contra a arquitetura de memory do sistema – as leituras de memory geralmente farão com que um cache ‘olhe para frente’ vários locais de memory após o atual em antecipação de uma leitura seqüencial. Se você estiver trabalhando para trás através da memory, o sistema de armazenamento em cache pode não antecipar leituras de um local de memory em um endereço de memory inferior. Nesse caso, é possível que o loop ‘para trás’ possa prejudicar o desempenho. No entanto, eu provavelmente ainda codificaria o loop dessa maneira (contanto que o desempenho não se torne um problema), porque a correção é primordial e fazer com que o código corresponda a um modelo é uma ótima maneira de ajudar a garantir a exatidão. O código incorreto é o menos 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 que e até que o desempenho do código realmente importe.

Em alguns processadores mais antigos há / foram instruções como DJNZ == “decrementar e pular se não for zero”. Isto permitia loops eficientes onde você carregava um valor de contagem inicial em um registrador e então você podia administrar efetivamente um loop de decrementação com uma instrução. Estamos falando de ISAs dos anos 80 aqui – seu professor está seriamente fora de contato se ele acha que essa “regra prática” ainda se aplica a CPUs modernas.

Prumo,

Não até que você esteja fazendo microoptimizações, quando você terá o manual do seu processador para entregar. Além disso, se você estivesse fazendo esse tipo de coisa, provavelmente não precisaria fazer essa pergunta de qualquer maneira. 🙂 Mas, seu professor, evidentemente, não concorda com essa idéia ….

Existem 4 coisas a considerar no seu exemplo de loop:

 for (i=N; i>=0; //thing 1 i--) //thing 2 { putchar('*'); //thing 3 } 
  • Comparação

A comparação é (como outros indicaram) relevante para arquiteturas de processadores específicos. 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.

  • Ajustamento

Em alguns casos, é mais rápido ajustar para cima ou para baixo. Normalmente, um bom compilador vai descobrir e refazer o loop, se puder. Nem todos os compiladores são bons.

  • Loop Body

Você está acessando um syscall com putchar. Isso é muito lento. Além disso, você está renderizando na canvas (indiretamente). Isso é ainda mais lento. Pense proporção de 1000: 1 ou mais. Nessa situação, o corpo do loop supera totalmente o custo do ajuste / comparação do loop.

  • Caches

Um layout de cache e memory pode ter um grande efeito no desempenho. Nesta situação, não importa. No entanto, se você estivesse acessando uma matriz e precisasse de um ótimo desempenho, caberia a você investigar como seu compilador e seu processador estabeleceram accesss à memory e ajustar seu software para aproveitar ao máximo isso. O exemplo de ações é o dado em relação à multiplicação de matrizes.

É uma questão interessante, mas, na prática, não acho que seja importante e não faça um loop melhor do que o outro.

De acordo com esta página da wikipedia: Leap second , “… o dia solar se torna 1,7 ms mais longo 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 minúscula 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 do motivo pelo qual a legibilidade é importante – eles não fazem o mesmo número de vezes.

Eu apostaria que a maioria dos programadores lêem (i = 0; 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 qualquer pensamento necessário.

Estranhamente, parece que existe uma diferença. Pelo menos, no PHP. Considere seguir o benchmark:

 < ?php print "
".PHP_VERSION; $iter = 100000000; $i=$t1=$t2=0; $t1 = microtime(true); for($i=0;$i< $iter;$i++){} $t2 = microtime(true); print '
$i++ : '.($t2-$t1); $t1 = microtime(true); for($i=$iter;$i>0;$i--){} $t2 = microtime(true); print '
$i-- : '.($t2-$t1); $t1 = microtime(true); for($i=0;$i< $iter;++$i){} $t2 = microtime(true); print '
++$i : '.($t2-$t1); $t1 = microtime(true); for($i=$iter;$i>0;--$i){} $t2 = microtime(true); print '
--$i : '.($t2-$t1);

Resultados são interessantes:

 PHP 5.2.13 $i++ : 8.8842368125916 $i-- : 8.1797409057617 ++$i : 8.0271911621094 --$i : 7.1027431488037 PHP 5.3.1 $i++ : 8.9625310897827 $i-- : 8.5790238380432 ++$i : 5.9647901058197 --$i : 5.4021768569946 

Se alguém souber 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 diferença?

Pode ser mais rápido.

No processador NIOS II com o qual estou trabalhando atualmente, o tradicional loop for

 for(i=0;i<100;i++) 

produz a assembly:

 ldw r2,-3340(fp) %load i to r2 addi r2,r2,1 %increase i by 1 stw r2,-3340(fp) %save value of i ldw r2,-3340(fp) %load value again (???) cmplti r2,r2,100 %compare if less than equal 100 bne r2,zero,0xa018 %jump 

Se contarmos abaixo

 for(i=100;i--;) 

nós temos um assembly que precisa de menos 2 instruções.

 ldw r2,-3340(fp) addi r3,r2,-1 stw r3,-3340(fp) bne r2,zero,0xa01c 

Se nós temos loops nesteds, onde o loop interno é executado muito, podemos ter uma diferença mensurável:

 int i,j,a=0; for(i=100;i--;){ for(j=10000;j--;){ a = j+1; } } 

Se o loop interno estiver escrito como acima, o tempo de execução será: 0.12199999999999999734 segundos. Se o laço interno é escrito da maneira tradicional, o tempo de execução é: 0.17199999999999998623 segundos. Portanto, a contagem regressiva do loop é 30% mais rápida.

Mas: esse teste foi feito com todas as otimizações do GCC desativadas. Se nós ligá-los, o compilador é realmente mais inteligente do que esta otimização Handish e ainda mantém o valor em um registro durante todo o ciclo e teríamos uma assembly como

 addi r2,r2,-1 bne r2,zero,0xa01c 

Neste exemplo em particular, o compilador percebe que a variável a será sempre 1 após a execução do loop e irá ignorar os loops no total.

No entanto, eu experimentei que, às vezes, se o corpo do loop é complexo o suficiente, o compilador não é capaz de fazer essa otimização, então a maneira mais segura de sempre obter uma execução rápida de loop é escrever:

 register int i; for(i=10000;i--;) { ... } 

É claro que isso só funciona, se não importa que o loop seja executado em sentido inverso e como Betamoo disse, somente se você estiver fazendo a contagem regressiva para zero.

Não, isso não é verdade. Uma situação em que poderia ser mais rápida é quando você chamaria uma function para verificar os limites durante cada iteração de um loop.

 for(int i=myCollection.size(); i >= 0; i--) { ... } 

Mas se é menos claro fazer assim, não vale a pena. Em linguagens modernas, você deve usar um loop foreach sempre que possível, de qualquer maneira. Você menciona especificamente o caso em que você deve usar um loop foreach – quando você não precisa do índice.

Independentemente da direção, use sempre o formulário de prefixo (++ i em vez de i ++)!

 for (i=N; i>=0; --i) 

ou

 for (i=0; i 

Explicação: http://www.eskimo.com/~scs/cclass/notes/sx7b.html

Além disso, você pode escrever

 for (i=N; i; --i) 

Mas eu esperaria que os compiladores modernos pudessem fazer exatamente essas otimizações.

O ponto é que, quando você está fazendo a contagem regressiva, não precisa checar i >= 0 separadamente para decrementar i . Observar:

 for (i = 5; i--;) { alert(i); // alert boxes showing 4, 3, 2, 1, 0 } 

Tanto a comparação quanto a redução i podem ser feitas na única expressão.

Veja outras respostas sobre por que isso se resume a menos instruções do x86.

Quanto a se faz uma diferença significativa em seu aplicativo, bem, eu acho que isso depende de quantos loops você tem e quão profundamente nesteds eles são. Mas para mim, é tão legível para fazer assim, então eu faço assim mesmo.

O que seu professor disse foi uma declaração oblíqua sem muito esclarecimento. NÃO é que decrementar seja mais rápido que incrementar, mas você pode criar um loop muito mais rápido com decréscimo do que com incremento.

Sem falar muito sobre isso, sem a necessidade de usar o contador de loops, etc – o que importa abaixo é apenas velocidade e contagem de loop (diferente de zero).

Aqui está como a maioria das pessoas implementa o loop com 10 iterações:

 int i; for (i = 0; i < 10; i++) { //something here } 

Para 99% dos casos, é tudo que se pode precisar, mas junto com PHP, PYTHON, JavaScript existe todo o mundo do software crítico (geralmente embutido, SO, jogos etc) onde os ticks do processador realmente importam, então olhe brevemente no código assembly:

 int i; for (i = 0; i < 10; i++) { //something here } 

após a compilation (sem otimização) versão compilada pode ser assim (VS2015):

 -------- C7 45 B0 00 00 00 00 mov dword ptr [i],0 -------- EB 09 jmp labelB labelA 8B 45 B0 mov eax,dword ptr [i] -------- 83 C0 01 add eax,1 -------- 89 45 B0 mov dword ptr [i],eax labelB 83 7D B0 0A cmp dword ptr [i],0Ah -------- 7D 02 jge out1 -------- EB EF jmp labelA out1: 

O loop inteiro é de 8 instruções (26 bytes). Nele - existem na verdade 6 instruções (17 bytes) com 2 ramificações. Sim sim eu sei que pode ser feito melhor (é apenas um exemplo).

Agora considere este constructo frequente que você encontrará frequentemente escrito pelo desenvolvedor incorporado:

 i = 10; do { //something here } while (--i); 

Também itera 10 vezes (sim, eu sei que o valor é diferente comparado com o loop mostrado, mas nos preocupamos com a contagem de iterações aqui). Isso pode ser compilado nisso:

 00074EBC C7 45 B0 01 00 00 00 mov dword ptr [i],1 00074EC3 8B 45 B0 mov eax,dword ptr [i] 00074EC6 83 E8 01 sub eax,1 00074EC9 89 45 B0 mov dword ptr [i],eax 00074ECC 75 F5 jne main+0C3h (074EC3h) 

5 instruções (18 bytes) e apenas um ramo. Na verdade, existem 4 instruções no loop (11 bytes).

A melhor coisa é que algumas CPUs (compatíveis com x86 / x64 incluídas) possuem instruções que podem decrementar um registrador, comparar o resultado com zero e executar o branch se o resultado for diferente de zero. Praticamente todos os processadores PC implementam esta instrução. Usando o loop é na verdade apenas uma instrução (sim, um) de 2 bytes:

 00144ECE B9 0A 00 00 00 mov ecx,0Ah label: // something here 00144ED3 E2 FE loop label (0144ED3h) // decrement ecx and jump to label if not zero 

Eu tenho que explicar o que é mais rápido?

Agora, mesmo se a CPU em particular não implementar a instrução acima, tudo o que é necessário para emular é um decréscimo seguido por salto condicional se o resultado da instrução anterior for zero.

Assim, independentemente de alguns casos que você pode apontar como um comentário porque estou errado, etc etc ENFERMEIOS - É BENÉFICO LOOP PARA BAIXO, se você sabe como, por que e quando.

PS. Sim, eu sei que o compilador sábio (com o nível de otimização apropriado) irá rewrite o loop (com o contador de loop crescente) no equivalente do..while para iterações de loop constantes ... (ou desenrole-o) ...

O que importa muito mais do que se você está aumentando ou diminuindo seu contador é se você está subindo na memory ou na memory. A maioria dos caches é otimizada para subir na memory, não na memory. Como o tempo de access à memory é o gargalo que a maioria dos programas enfrenta atualmente, isso significa que alterar o programa para que você possa aumentar a memory pode resultar em um aumento de desempenho, mesmo se isso exigir que você compare seu contador com um valor diferente de zero. Em alguns dos meus programas, eu vi uma melhoria significativa no desempenho, alterando o meu código para ir para a memory, em vez de para baixo.

Cético? Aqui está a saída que recebi:

 Ave. Up Memory = 4839 mus Ave. Down Memory = 5552 mus Ave. Up Memory = 18638 mus Ave. Down Memory = 19053 mus 

da execução deste programa:

 #include  #include  #include  #include  template void FillWithRandomNumbers(Iterator start, Iterator one_past_end, T a, T b) { std::random_device rnd_device; std::mt19937 generator(rnd_device()); std::uniform_int_distribution dist(a, b); for (auto it = start; it != one_past_end; it++) *it = dist(generator); return ; } template void FillWithRandomNumbers(Iterator start, Iterator one_past_end, double a, double b) { std::random_device rnd_device; std::mt19937_64 generator(rnd_device()); std::uniform_real_distribution dist(a, b); for (auto it = start; it != one_past_end; it++) *it = dist(generator); return ; } template inline void sum_abs_up(Iterator first, Iterator one_past_last, T &total) { T sum = 0; auto it = first; do { sum += *it; it++; } while (it != one_past_last); total += sum; } template inline void sum_abs_down(Iterator first, Iterator one_past_last, T &total) { T sum = 0; auto it = one_past_last; do { it--; sum += *it; } while (it != first); total += sum; } template std::chrono::nanoseconds TimeDown(std::vector &vec, const std::vector &vec_original, std::size_t num_repititions, T &running_sum) { std::chrono::nanoseconds total{0}; for (std::size_t i = 0; i < num_repititions; i++) { auto start_time = std::chrono::high_resolution_clock::now(); sum_abs_down(vec.begin(), vec.end(), running_sum); total += std::chrono::high_resolution_clock::now() - start_time; vec = vec_original; } return total; } template std::chrono::nanoseconds TimeUp(std::vector &vec, const std::vector &vec_original, std::size_t num_repititions, T &running_sum) { std::chrono::nanoseconds total{0}; for (std::size_t i = 0; i < num_repititions; i++) { auto start_time = std::chrono::high_resolution_clock::now(); sum_abs_up(vec.begin(), vec.end(), running_sum); total += std::chrono::high_resolution_clock::now() - start_time; vec = vec_original; } return total; } template void TimeFunctions(std::size_t num_repititions, std::size_t vec_size = (1u < < 24)) { auto lower = std::numeric_limits::min(); auto upper = std::numeric_limits::max(); std::vector vec(vec_size); FillWithRandomNumbers(vec.begin(), vec.end(), lower, upper); const auto vec_original = vec; ValueType sum_up = 0, sum_down = 0; auto time_up = TimeUp(vec, vec_original, num_repititions, sum_up).count(); auto time_down = TimeDown(vec, vec_original, num_repititions, sum_down).count(); std::cout < < "Ave. Up Memory = " << time_up/(num_repititions * 1000) << " mus\n"; std::cout << "Ave. Down Memory = " << time_down/(num_repititions * 1000) << " mus" << std::endl; return ; } int main() { std::size_t num_repititions = 1 << 10; TimeFunctions(num_repititions); std::cout < < '\n'; TimeFunctions(num_repititions); return 0; } 

Both sum_abs_up and sum_abs_down do the same thing and are timed they same way with the only difference being that sum_abs_up goes up memory while sum_abs_down goes down memory. I even pass vec by reference so that both functions access the same memory locations. Nevertheless, sum_abs_up is consistently faster than sum_abs_down . Give it a run yourself (I compiled it with g++ -O3).

FYI vec_original is there for experimentation, to make it easy for me to change sum_abs_up and sum_abs_down in a way that makes them alter vec while not allowing these changes to affect future timings.

It’s important to note how tight the loop that I’m timing is. If a loop’s body is large then it likely won’t matter whether its iterator goes up or down memory since the time it takes to execute the loop’s body will likely completely dominate. Also, it’s important to mention that with some rare loops, going down memory is sometimes faster than going up it. But even with such loops it’s rarely ever the case that going up was always slower than going down (unlike small-bodied loops that go up memory, for which the opposite is frequently true; in fact, for a small handful of loops I’ve timed, the increase in performance by going up memory was 40+%).

The point is, as a rule of thumb, if you have the option, if the loop’s body is small, and if there’s little difference between having your loop go up memory instead of down it, then you should go up memory.

Now, I think you had enough assembly lectures:) I would like to present you another reason for top->down approach.

The reason to go from the top is very simple. In the body of the loop, you might accidentally change the boundary, which might end in incorrect behaviour or even non-terminating loop.

Look at this small portion of Java code (the language does not matter I guess for this reason):

  System.out.println("top->down"); int n = 999; for (int i = n; i >= 0; i--) { n++; System.out.println("i = " + i + "\tn = " + n); } System.out.println("bottom->up"); n = 1; for (int i = 0; i < n; i++) { n++; System.out.println("i = " + i + "\tn = " + n); } 

So my point is you should consider prefering going from the top down or having a constant as a boundary.

At an assembler level a loop that counts down to zero is generally slightly faster than one that counts up to a given value. If the result of a calculation is equal to zero most processors will set a zero flag. If subtracting one makes a calculation wrap around past zero this will normally change the carry flag (on some processors it will set it on others it will clear it), so the comparison with zero comes essentially for free.

This is even more true when the number of iterations is not a constant but a variable.

In trivial cases the compiler may be able to optimise the count direction of a loop automatically but in more complex cases it may be that the programmer knows that the direction of the loop is irrelevant to the overall behaviour but the compiler cannot prove that.