É mais rápido contagem regressiva do que contagem regressiva?

131

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ê?

Prumo
fonte
6
Qual cientista da computação? Em que publicação?
bmargulies
26
É concebível que você possa economizar um nanossegundo por iteração, ou quase um fio de cabelo em uma família de mamutes lanudos. Ele putcharestá usando 99,9999% do tempo (mais ou menos).
Mike Dunlavey
38
Otimização prematura é a raiz de todo o mal. Use a forma que lhe parecer mais adequada, porque (como você já sabe) são logicamente equivalentes. A parte mais difícil da programação é comunicar a teoria do programa a outros programadores (e a você!). Usar uma construção que faça você ou algum outro programador examiná-la por mais de um segundo é uma perda líquida. Você nunca recuperará o tempo que alguém passa pensando "por que isso é contado?"
David M
61
O primeiro loop é obviamente mais lento, pois chama putchar 11 vezes, enquanto o segundo chama apenas 10 vezes.
Paul Kuliniewicz
17
Você notou que, se inão está assinado, o primeiro loop é infinito?
21412 Shahbaz

Respostas:

371

É mesmo verdade? e se sim, alguém sabe por quê?

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?

  • 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 definir um sinalizador (provavelmente apenas em máquinas da família x86), talvez você nem precise de uma instrução de comparação explícita

É 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.

Norman Ramsey
fonte
3
++ E, além disso, o processo putcharleva muitas ordens de magnitude a mais do que a sobrecarga do loop.
Mike Dunlavey
41
Não é estritamente mitologia: se ele está fazendo algum tipo de sistema em tempo real super otimizado, seria útil. Mas esse tipo de hacker provavelmente já saberia tudo isso e certamente não confundiria estudantes de nível básico com arcanos.
Paul Nathan
4
@ Josué: De que maneira essa otimização seria detectável? Como disse o interlocutor, o índice do loop não é usado no próprio loop, portanto, desde que o número de iterações seja o mesmo, não haverá mudança no comportamento. Em termos de uma prova de correção, a substituição da variável j=N-imostra que os dois loops são equivalentes.
Psmears
7
+1 para o resumo. Não se preocupe, porque no hardware moderno isso praticamente não faz diferença. Também não fez praticamente nenhuma diferença há 20 anos. Se você acha que precisa se preocupar, pense nos dois lados, não veja nenhuma diferença clara e volte a escrever o código de forma clara e correta .
Donal Fellows
3
Não sei se devo votar no corpo ou votar no resumo.
Danubian Sailor
29

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<Ncada 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 se i>=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.)

sigfpe
fonte
2
não seria essa a bandeira zero e não a bandeira de transporte?
Bob
2
@ Bob Neste caso, você pode querer chegar a zero, imprimir um resultado, diminuir ainda mais e depois descobrir que ficou abaixo de zero, causando uma transferência (ou um empréstimo). Mas, escrito de maneira um pouco diferente, um loop decrescente pode usar o sinalizador zero.
Sigfpe
1
Para ser perfeitamente pedante, nem todo o hardware moderno é canalizado. Os processadores embarcados terão muito mais relevância para esse tipo de microoptimização.
Paul Nathan
@ Paul Como eu tenho alguma experiência com Atmel AVRs eu não esquecer de mencionar microcontroladores ...
SIGFPE
27

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.

dthorpe
fonte
2
Eu já vi o compilador C ++ da Microsoft há alguns anos fazer essa otimização. É capaz de ver que o índice de loop não é usado e, portanto, reorganiza-o da forma mais rápida.
Mark Ransom
1
@ Mark: O compilador Delphi também, a partir de 1996.
dthorpe
4
@MarkRansom Na verdade, o compilador pode implementar o loop usando a contagem regressiva, mesmo que a variável de índice do loop seja usada, dependendo de como é usada no loop. Se a variável de índice de loop for usada apenas para indexar em matrizes estáticas (matrizes de tamanho conhecido em tempo de compilação), a indexação da matriz poderá ser feita como ptr + tamanho da matriz - índice de loop var, que ainda pode ser uma única instrução em x86. É muito difícil depurar o assembler e ver o loop decrescente, mas os índices da matriz subindo!
dthorpe
1
Atualmente, seu compilador provavelmente não usará as instruções loop e jecxz, pois são mais lentas que um par dec / jnz.
fuz 15/07/2013
1
@FUZxxl Mais uma razão para não escrever seu loop de maneiras estranhas. Escreva um código claro legível por humanos e deixe o compilador fazer seu trabalho.
dthorpe
23

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

i>=0
i<N

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:

  1. Carregar i
  2. Compare e pule se Menor ou igual a zero

Mas o segundo precisa carregar N da memória toda vez

  1. carregar i
  2. carregar N
  3. Sub ie N
  4. Compare e pule se Menor ou igual a zero

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

  • Observe que hoje em dia os compiladores podem fazer essa otimização para você (se for inteligente o suficiente)
  • Observe também que o oleoduto pode causar o efeito de anomalia de Belady (não pode ter certeza do que será melhor)
  • Por fim: observe que os 2 loops que você apresentou não são equivalentes. O primeiro imprime mais um *.

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

Betamoo
fonte
6
então o que você está dizendo é que não é mais rápido a contagem regressiva, é mais rápido comparar com zero do que qualquer outro valor. Significando contar de 10 a 100 e contagem decrescente de 100 a 10 seria o mesmo?
Bob
8
Sim .. não é a questão de "contagem baixo ou para cima" .. mas é a questão de "comparando com o que" ..
Betamoo
3
Enquanto isso é verdade, o nível do assembler. Duas coisas se combinam para me mostrar falso na realidade - o hardware moderno que usa tubos longos e instruções especulativas se infiltra no "Sub i e N" sem incorrer em um ciclo extra - e - até o compilador mais grosseiro otimiza o "Sub i e N" N "fora de existência.
James Anderson
2
@nico Não precisa ser um sistema antigo. Ele só precisa ser um conjunto de instruções em que haja uma operação de comparação com zero, que é de alguma forma mais rápida / melhor que a comparação equivalente ao valor do registro. o x86 possui em jcxz. x64 ainda o possui. Não é antigo. Além disso, as arquiteturas RISC costumam zero caso especial. O chip DEC AXP Alpha (na família MIPS), por exemplo, tinha um "registro zero" - lido como zero, a gravação não faz nada. A comparação com o registro zero em vez de com um registro geral que contém um valor zero reduz as dependências entre instruções e ajuda na execução fora de ordem.
dthorpe
5
@ Betamoo: Muitas vezes me pergunto por que as respostas melhores / mais corretas (que são suas) não são mais apreciadas por mais votos e concluem que com muita frequência os votos no stackoverflow são influenciados pela reputação (em pontos) de uma pessoa que responde ( que é muito, muito ruim) e não pela resposta correção
Artur
12

Em C para psudo-montagem:

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 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

x = x - 0

é semanticamente o mesmo que

compare x, 0

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.

nategoose
fonte
É possível que exista um diff de 2 instruções em vez de apenas 1?
Pacerier 12/08/19
Além disso, por que é difícil ter certeza disso? Contanto que o var inão seja usado dentro do loop, obviamente você pode inverter isso, não é?
Pacerier 12/08/19
6

Contagem regressiva mais rápida em casos como este:

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

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:

size = someObject.getAllObjects.size();
for (i = 0; i < size; i++) {…}
0x2D9A3
fonte
5
Não é "definitivamente mais rápido". Em muitos casos, a chamada size () poderia ser retirada do loop durante a contagem, portanto, ainda seria chamada apenas uma vez. Obviamente, isso depende da linguagem e do compilador (e depende do código; por exemplo, em C ++, não será alterado se size () for virtual), mas está longe de ser definido de qualquer maneira.
12128 Peter
3
@ Peter: Somente se o compilador souber ao certo que size () é idempotente no loop. Provavelmente esse nem sempre é o caso, a menos que o loop seja muito simples.
Lawrence Dol
@LawrenceDol, O compilador definitivamente o conhecerá, a menos que você tenha um código dinâmico usando o compilador exec.
Pacerier 12/08/19
4

É mais rápido contar em contagem regressiva do que subir?

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 Nitens 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.

Michael Burr
fonte
3

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.

Paul R
fonte
3

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:

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 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.

  • Ajustamento

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.

  • Corpo do laço

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.

  • Caches

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.

Paul Nathan
fonte
3

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:

Average Up Memory   = 4839 mus
Average Down Memory = 5552 mus

Average Up Memory   = 18638 mus
Average Down Memory = 19053 mus

(em que "mus" significa microssegundos) da execução deste programa:

#include <chrono>
#include <iostream>
#include <random>
#include <vector>

//Sum all numbers going up memory.
template<class Iterator, class T>
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;
}

//Sum all numbers going down memory.
template<class Iterator, class T>
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;
}

//Time how long it takes to make num_repititions identical calls to sum_abs_down().
//We will divide this time by num_repitions to get the average time.
template<class T>
std::chrono::nanoseconds TimeDown(std::vector<T> &vec, const std::vector<T> &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<class T>
std::chrono::nanoseconds TimeUp(std::vector<T> &vec, const std::vector<T> &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<class Iterator, typename T>
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<T> dist(a, b);
  for (auto it = start; it != one_past_end; it++)
    *it = dist(generator);
  return ;
}

template<class Iterator>
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<double> dist(a, b);
  for (auto it = start; it != one_past_end; it++)
    *it = dist(generator);
  return ;
}

template<class ValueType>
void TimeFunctions(std::size_t num_repititions, std::size_t vec_size = (1u << 24)) {
  auto lower = std::numeric_limits<ValueType>::min();
  auto upper = std::numeric_limits<ValueType>::max();
  std::vector<ValueType> 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 << "Average Up Memory   = " << time_up/(num_repititions * 1000) << " mus\n";
  std::cout << "Average Down Memory = " << time_down/(num_repititions * 1000) << " mus"
            << std::endl;
  return ;
}

int main() {
  std::size_t num_repititions = 1 << 10;
  TimeFunctions<int>(num_repititions);
  std::cout << '\n';
  TimeFunctions<double>(num_repititions);
  return 0;
}

Ambos sum_abs_upe sum_abs_downfazem a mesma coisa (soma o vetor de números) e são cronometrados da mesma maneira, com a única diferença que sum_abs_upaumenta a memória e sum_abs_downdiminui a memória. Eu passo até vecpor referência para que ambas as funções acessem os mesmos locais de memória. No entanto, sum_abs_upé consistentemente mais rápido que sum_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_originalexiste para a experimentação, para facilitar a mudança sum_abs_upe sum_abs_downde uma maneira que as altere vec, sem permitir que essas mudanças afetem os horários futuros. Eu recomendo a brincar com sum_abs_upe sum_abs_downe cronometrando os resultados.

Matthew K.
fonte
2

independentemente da direção, sempre use o formato de prefixo (++ i em vez de i ++)!

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

ou

for (i=0; i<N; ++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 sejam capazes de fazer exatamente essas otimizações.

RSabet
fonte
Nunca vi pessoas reclamarem disso antes. Mas depois de ler o link, na verdade, faz sentido :) Obrigado.
Tommy Jakobsen
3
Por que ele sempre deveria usar o formulário de prefixo? Se não houver nenhuma tarefa em andamento, elas serão idênticas, e o artigo ao qual você vinculou diz que o formulário do postfix é mais comum.
22410 BobDevil
3
Por que sempre se deve usar o formulário de prefixo? Nesse caso, é semanticamente idêntico.
Ben Zotto
2
O formulário do postfix pode potencialmente criar uma cópia desnecessária do objeto, embora, se o valor nunca estiver sendo usado, o compilador provavelmente o otimizará para o formulário do prefixo.
Nick Lewis
Por força do hábito, eu sempre faço --i e i ++ porque, quando eu aprendia, os computadores C geralmente apresentavam predileção e pós-incremento de registro, mas não vice-versa. Assim, * p ++ e * - p foram mais rápidos que * ++ p e * p-- porque os dois primeiros podiam ser executados em uma instrução de código de máquina 68000.
JeremyP
2

É 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.

Jim Flood
fonte
As duas construções são tão fáceis de entender. Algumas pessoas afirmam que, se você tiver 3 ou 4 repetições, é melhor copiar a instrução do que fazer um loop, porque é mais fácil de entender.
Danubian Sailor
2

Estranhamente, parece que há uma diferença. Pelo menos em PHP. Considere a seguinte referência:

<?php

print "<br>".PHP_VERSION;
$iter = 100000000;
$i=$t1=$t2=0;

$t1 = microtime(true);
for($i=0;$i<$iter;$i++){}
$t2 = microtime(true);
print '<br>$i++ : '.($t2-$t1);

$t1 = microtime(true);
for($i=$iter;$i>0;$i--){}
$t2 = microtime(true);
print '<br>$i-- : '.($t2-$t1);

$t1 = microtime(true);
for($i=0;$i<$iter;++$i){}
$t2 = microtime(true);
print '<br>++$i : '.($t2-$t1);

$t1 = microtime(true);
for($i=$iter;$i>0;--$i){}
$t2 = microtime(true);
print '<br>--$i : '.($t2-$t1);

Os 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 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?

ts.
fonte
O motivo é mais lento é que o operador de prefixo não precisa armazenar temporariamente. Considere $ foo = $ i ++; Três coisas acontecem: $ i é armazenado em um temporário, $ i é incrementado e, em seguida, $ foo recebe o valor desse temporário. No caso de $ i ++; um compilador inteligente pode perceber que o temporário é desnecessário. PHP simplesmente não. Os compiladores C ++ e Java são inteligentes o suficiente para fazer essa otimização simples.
Compilador conspícuo
e por que $ i-- é mais rápido que $ i ++?
ts.
Quantas iterações do seu benchmark você executou? Você reclamou estranhos e fez uma média para cada resultado? Seu computador estava fazendo outra coisa durante os benchmarks? Essa diferença de ~ 0,5 poderia ser apenas o resultado de outra atividade da CPU, ou utilização de pipeline, ou ... ou ... bem, você entendeu.
Guru de oito bits
Sim, aqui estou dando médias. O benchmark foi executado em máquinas diferentes e a diferença é acidental.
ts.
@ Compilador Conspícuo => você sabe ou supõe?
ts.
2

Ele 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 montagem:

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

for(i=100;i--;)

temos uma montagem que precisa de 2 instruções a menos.

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

Se tivermos loops aninhados, 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 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

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

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:

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, apenas se você estiver contando até zero.

user2998086
fonte
2

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:

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

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:

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

após a compilação (sem otimização), a 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 - 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:

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

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:

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).

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:

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 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) ...

Artur
fonte
1

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.

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

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.

Jonathon Faust
fonte
1
Para ser claro e eficiente, você deve ter pelo menos o hábito for(int i=0, siz=myCollection.size(); i<siz; i++).
Lawrence Dol
1

O ponto é que, ao fazer uma contagem regressiva, você não precisa verificar i >= 0separadamente para diminuir i. Observar:

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

A comparação e o decremento ipodem 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.

thomasrutter
fonte
Acho que esse estilo é ruim, porque depende do leitor saber que o valor de retorno de i - é o valor antigo de i, para o possível valor de salvar um ciclo. Isso seria significativo se houvesse muitas iterações de loop, e o ciclo fosse uma fração significativa do comprimento da iteração e realmente aparecesse no tempo de execução. Em seguida, alguém tentará (i = 5; --i;) porque ouviu dizer que em C ++ você pode evitar criar temporários quando eu for do tipo não trivial, e agora você está em um país com insetos jogou fora sua oportunidade de fazer com que código errado parecesse errado.
mabraham
0

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):

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

Portanto, o que quero dizer é que você deve considerar preferir ir de cima para baixo ou ter uma constante como limite.

Gabriel Ščerbák
fonte
Hã?!! Seu exemplo fracassado é realmente contra-intuitivo, ou seja, um argumento do tipo palhaço - ninguém jamais escreveria isso. Alguém escreveria for (int i=0; i < 999; i++) {.
Lawrence Dol
O @Software Monkey imagina que n é o resultado de alguma computação ... por exemplo, você pode repetir alguma coleção e seu tamanho é o limite, mas como efeito colateral, você adiciona novos elementos à coleção no corpo do loop.
Gabriel Ščerbák
Se é isso que você pretendia comunicar, é isso que seu exemplo deve ilustrar:for(int xa=0; xa<collection.size(); xa++) { collection.add(SomeObject); ... }
Lawrence Dol
@Software Monkey Eu queria ser mais geral do que apenas falar particularmente sobre coleções, porque o que estou
pensando
2
Sim, mas se você quiser argumentar com exemplos, seus exemplos precisam ser confiáveis ​​e ilustrativos.
Lawrence Dol
-1

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.

plugwash
fonte