As instruções condicionais não triviais devem ser movidas para a seção de inicialização dos loops?

21

Eu recebi essa ideia dessa pergunta no stackoverflow.com

O seguinte padrão é comum:

final x = 10;//whatever constant value
for(int i = 0; i < Math.floor(Math.sqrt(x)) + 1; i++) {
  //...do something
}

O que estou tentando enfatizar é que a declaração condicional é algo complicado e não muda.

É melhor declará-lo na seção de inicialização do loop, como tal?

final x = 10;//whatever constant value
for(int i = 0, j = Math.floor(Math.sqrt(x)) + 1; i < j; i++) {
  //...do something
}

Isso é mais claro?

E se a expressão condicional for simples, como

final x = 10;//whatever constant value
for(int i = 0, j = n*n; i > j; j++) {
  //...do something
}
Celeritas
fonte
47
Por que não movê-lo para a linha antes do loop, também pode dar um nome sensato.
jonrsharpe
2
@ Mehrdad: Eles não são equivalentes. Se xé grande em magnitude, Math.floor(Math.sqrt(x))+1é igual a Math.floor(Math.sqrt(x)). :-)
R. ..
5
@ Jonrsharpe Porque isso ampliaria o escopo da variável. Não estou necessariamente dizendo que é uma boa razão para não fazê-lo, mas essa é a razão pela qual algumas pessoas não.
Kevin Krumwiede
3
@KevinKrumwiede Se o escopo é uma preocupação, limite-o colocando o código em seu próprio bloco, por exemplo, { x=whatever; for (...) {...} }ou, melhor ainda, considere se há o suficiente acontecendo para que ele precise ser uma função separada.
Blrfl
2
@jonrsharpe Você também pode dar um nome sensato ao declará-lo na seção init. Não estou dizendo que eu colocaria lá; ainda é mais fácil ler se estiver separado.
JollyJoker

Respostas:

62

O que eu faria é algo como isto:

void doSomeThings() {
    final x = 10;//whatever constant value
    final limit = Math.floor(Math.sqrt(x)) + 1;
    for(int i = 0; i < limit; i++) {
         //...do something
    }
}

Honestamente, a única boa razão para empilhar a inicialização j(agora limit) no cabeçalho do loop é mantê-lo no escopo correto. Tudo o que é necessário para fazer com que um problema não seja um bom escopo de fechamento.

Posso apreciar o desejo de ser rápido, mas não sacrifique a legibilidade sem uma boa razão.

Certamente, o compilador pode otimizar, inicializar vários vars pode ser legal, mas os loops são difíceis o suficiente para depurar como estão. Por favor, seja gentil com os humanos. Se isso realmente estiver nos atrasando, é bom entender o suficiente para corrigi-lo.

candied_orange
fonte
Bom ponto. Eu assumiria que não se incomodaria com outra variável se a expressão fosse algo simples; por exemplo, for(int i = 0; i < n*n; i++){...}você não atribuiria n*na uma variável, sim?
Celeritas
1
Eu faria e eu tenho. Mas não por velocidade. Para legibilidade. O código legível tende a ser rápido por si só.
3141616
1
Até a questão do escopo desaparece se você marcar como uma constante ( final). Quem se importa se uma constante que tem imposição do compilador impedindo que ela seja alterada esteja acessível posteriormente na função?
jpmc26
Eu acho que uma coisa importante é o que você espera que aconteça. Eu sei que o exemplo usa o sqrt, mas e se fosse outra função? A função é pura? Você está sempre esperando os mesmos valores? Existem efeitos colaterais? Os efeitos colaterais são algo que você pretende que ocorra em todas as iterações?
Pieter B
38

Um bom compilador irá gerar o mesmo código de qualquer maneira; portanto, se você estiver buscando desempenho, faça apenas uma alteração se estiver em um loop crítico e você realmente o criou e fez o perfil e descobriu que faz a diferença. Mesmo que o compilador não possa otimizá-lo, como as pessoas apontaram nos comentários sobre o caso com chamadas de função, na grande maioria das situações, a diferença de desempenho será muito pequena para valer a consideração de um programador.

Contudo...

Não devemos esquecer que o código é principalmente um meio de comunicação entre seres humanos, e as duas opções não se comunicam muito bem com outros seres humanos. O primeiro dá a impressão de que a expressão precisa ser calculada a cada iteração, e o segundo na seção de inicialização implica que ela será atualizada em algum lugar dentro do loop, onde é realmente constante o tempo todo.

Na verdade, eu preferiria que fosse retirado acima do loop e tornado finalisso imediato e abundantemente claro para quem lê o código. Isso também não é ideal porque aumenta o escopo da variável, mas sua função de fechamento não deve conter muito mais do que esse loop.

Karl Bielefeldt
fonte
5
Quando você começa a incluir chamadas de função, fica muito mais difícil otimizar o compilador. O compilador precisaria ter um conhecimento especial de que Math.sqrt não tem efeitos colaterais.
Peter Green
@PeterGreen Se for Java, a JVM pode descobrir, mas pode demorar um pouco.
chrylis -on strike-
A pergunta está marcada com C ++ e Java. Eu não sei o quão avançada é a JVM do Java e quando ele pode e não pode descobrir, mas eu sei que os compiladores C ++ em geral não podem descobrir ainda uma função é pura, a menos que tenha uma anotação não padrão informando o compilador portanto, ou o corpo da função é visível e todas as funções chamadas indiretamente podem ser detectadas como puras pelo mesmo critério. Nota: sem efeitos colaterais não é suficiente para tirá-lo da condição de loop. As funções que dependem do estado global não podem ser movidas para fora do loop se o corpo do loop puder modificar o estado.
hvd
Torna-se interessante quando Math.sqrt (x) é substituído por Mymodule.SomeNonPureMethodWithSideEffects (x).
Pieter B
9

Como o @Karl Bielefeldt disse em sua resposta, isso geralmente não é um problema.

No entanto, era um problema comum em C e C ++, e um truque surgiu para contornar o problema sem reduzir a legibilidade do código - iterar para trás, até0 .

final x = 10;//whatever constant value
for(int i = Math.floor(Math.sqrt(x)); i >= 0; i--) {
  //...do something
}

Agora, o condicional em cada iteração é exatamente o >= 0que todo compilador compilará em 1 ou 2 instruções de montagem. Cada CPU fabricada nas últimas décadas deve ter verificações básicas como estas; fazendo uma verificação rápida na minha máquina x64, vejo que isso se previsivelmente se transforma em cmpl $0x0, -0x14(%rbp)(valor de comparação de longa duração 0 vs. registro rbp compensado -14) e jl 0x100000f59(pule para a instrução que segue o loop, se a comparação anterior for verdadeira para "2nd-arg <1-arg ") .

Observe que eu removi o + 1de Math.floor(Math.sqrt(x)) + 1; para que a matemática funcione, o valor inicial deve ser int i = «iterationCount» - 1. Também digno de nota é que seu iterador deve ser assinado; unsigned intnão funcionará e provavelmente alertará o compilador.

Depois de programar em linguagens baseadas em C por ~ 20 anos, agora apenas escrevo loops de iteração de índice reverso, a menos que haja um motivo específico para iterar a indexação direta. Além de verificações mais simples nos condicionais, a iteração reversa geralmente também evita o que seria mutável de matriz de array enquanto iterava.

Slipp D. Thompson
fonte
1
Tudo o que você escreve é ​​tecnicamente correto. O comentário sobre as instruções pode ser enganador, já que todas as CPUs modernas também foram projetadas para funcionar bem com loops de iteração direta, independentemente de terem uma instrução especial para elas. De qualquer forma, a maior parte do tempo geralmente é gasta dentro do loop, não executando a iteração.
Jørgen Fogh
4
Deve-se notar que as otimizações modernas do compilador são projetadas com o código "normal" em mente. Na maioria dos casos, o compilador gerará código com a mesma rapidez, independentemente de você usar "truques" de otimização ou não. Mas alguns truques podem realmente atrapalhar a otimização, dependendo de quão complicados eles sejam. Se o uso de certos truques torna seu código mais legível ou ajuda a detectar erros comuns, isso é ótimo, mas não se iluda pensando "se eu escrever código dessa maneira, será mais rápido". Escreva o código como faria normalmente e, em seguida, crie um perfil para encontrar locais onde você precisa otimizar.
0x5453
Observe também que os unsignedcontadores funcionarão aqui se você modificar a verificação (a maneira mais fácil é adicionar o mesmo valor aos dois lados); por exemplo, para qualquer decréscimo Dec, a verificação (i + Dec) >= Decdeve sempre ter o mesmo resultado que a signedverificação i >= 0, com ambos signede unsignedcontadores, desde que o idioma tenha regras abrangentes bem definidas para unsignedvariáveis ​​(especificamente, -n + n == 0deve ser verdade para ambas signede unsigned). Observe, no entanto, que isso pode ser menos eficiente que uma >=0verificação assinada , se o compilador não tiver uma otimização para isso.
Justin Time - Restabelece Monica
1
@ JustinTime Sim, o requisito assinado é a parte mais difícil; adicionar uma Decconstante ao valor inicial e ao valor final funciona, mas o torna muito menos intuitivo e, se estiver usando icomo um índice de matriz, você também precisará fazer um unsigned int arrayI = i - Dec;no corpo do loop. Eu apenas uso a iteração direta quando preso a um iterador não assinado; geralmente com a i <= count - 1condição de manter a lógica paralela aos loops de iteração reversa.
Slipp D. Thompson
1
@ SlippD.Thompson Eu não quis dizer adicionar Decespecificamente os valores inicial e final, mas alterar a verificação da condição Decdos dois lados. for (unsigned i = N - 1; i + 1 >= 1; i--) /*...*/ Isso permite que você use inormalmente dentro do loop, garantindo ao mesmo tempo o menor valor possível no lado esquerdo da condição 0(para evitar interferências na envolvente). É definitivamente muito mais simples usar a iteração direta ao trabalhar com contadores não assinados.
Justin Time - Restabelece Monica
3

Torna-se interessante quando Math.sqrt (x) é substituído por Mymodule.SomeNonPureMethodWithSideEffects (x).

Basicamente, meu modus operandi é: se algo deve sempre dar o mesmo valor, avalie-o apenas uma vez. Por exemplo List.Count, se a lista não mudar durante a operação do loop, coloque a contagem fora do loop em outra variável.

Algumas dessas "contagens" podem ser surpreendentemente caras, especialmente quando você está lidando com bancos de dados. Mesmo se você estiver trabalhando em um conjunto de dados que não deve ser alterado durante a iteração da lista.

Pieter B
fonte
Quando a contagem é cara, você não deve usá-la. Em vez disso, você deve fazer o equivalente a:for( auto it = begin(dataset); !at_end(it); ++it )
Ben Voigt
@BenVoigt Usar um iterador é definitivamente a melhor maneira para essas operações. Eu apenas mencionei isso para ilustrar meu argumento sobre o uso de métodos não puros com efeitos colaterais.
Pieter B
0

Na minha opinião, isso é altamente específico do idioma. Por exemplo, se estiver usando C ++ 11, eu suspeitaria que, se a verificação da condição fosse uma constexprfunção, o compilador provavelmente otimizaria as várias execuções, pois sabe que produzirá o mesmo valor todas as vezes.

No entanto, se a chamada de função for uma função de biblioteca que não é constexpro compilador, quase certamente a executará em todas as iterações, pois não pode deduzir isso (a menos que esteja em linha e, portanto, possa ser deduzida como pura).

Eu sei menos sobre Java, mas, como é compilado pelo JIT, acho que o compilador tem informações suficientes em tempo de execução para provavelmente incorporar e otimizar a condição. Mas isso depende de um bom design do compilador e o compilador que decide esse loop é uma prioridade de otimização, a qual podemos adivinhar.

Pessoalmente, eu sinto que é um pouco mais elegante para colocar a condição dentro do loop for, se puder, mas se ele é complexo vou escrevê-lo em um constexprou inlinefunção, ou suas línguas equivalant sugerir que a função é pura e optimisable. Isso torna a intenção óbvia e mantém o estilo de loop idiomático sem criar uma linha enorme e ilegível. Também fornece um nome à verificação de condição, se for sua própria função, para que os leitores possam ver imediatamente de que serve a verificação sem lê-la, se for complexa.

Validade
fonte