Sempre que eu precisar de divisão, por exemplo, verificação de condição, gostaria de refatorar a expressão de divisão em multiplicação, por exemplo:
Versão original:
if(newValue / oldValue >= SOME_CONSTANT)
Nova versão:
if(newValue >= oldValue * SOME_CONSTANT)
Porque acho que pode evitar:
Divisão por zero
Estouro quando
oldValue
é muito pequeno
Isso está certo? Existe algum problema para esse hábito?
coding-style
language-agnostic
math
ocomfd
fonte
fonte
oldValue >= 0
?Respostas:
Dois casos comuns a serem considerados:
Aritmética de número inteiro
Obviamente, se você estiver usando aritmética inteira (que trunca), obterá um resultado diferente. Aqui está um pequeno exemplo em c #:
Resultado:
Aritmética de ponto flutuante
Além do fato de que a divisão pode gerar um resultado diferente quando divide por zero (gera uma exceção, enquanto a multiplicação não), também pode resultar em erros de arredondamento ligeiramente diferentes e em um resultado diferente. Exemplo simples em C #:
Resultado:
Caso você não acredite em mim, aqui está um violino que você pode executar e ver por si mesmo.
Outros idiomas podem ser diferentes; lembre-se, no entanto, que o C #, como muitas linguagens, implementa uma biblioteca de ponto flutuante padrão IEEE (IEEE 754) , portanto, você deve obter os mesmos resultados em outros tempos de execução padronizados.
Conclusão
Se você está trabalhando no greenfield , provavelmente está bem.
Se você estiver trabalhando no código legado, e o aplicativo for um aplicativo financeiro ou outro sensível que executa aritmética e é necessário para fornecer resultados consistentes, tenha muito cuidado ao mudar as operações. Se necessário, verifique se você possui testes de unidade que detectam alterações sutis na aritmética.
Se você está apenas fazendo coisas como contar elementos em uma matriz ou outras funções computacionais gerais, provavelmente estará bem. Não tenho certeza se o método de multiplicação deixa seu código mais claro.
Se você estiver implementando um algoritmo em uma especificação, eu não mudaria nada, não apenas por causa do problema de erros de arredondamento, mas para que os desenvolvedores possam revisar o código e mapear cada expressão de volta à especificação para garantir que não haja implementação falhas.
fonte
Gostei da sua pergunta, pois potencialmente abrange muitas idéias. No geral, eu suspeito que a resposta é que depende , provavelmente dos tipos envolvidos e da possível faixa de valores em seu caso específico.
Meu instinto inicial é refletir sobre o estilo , ie. sua nova versão é menos clara para o leitor do seu código. Eu imagino que teria que pensar por um segundo ou dois (ou talvez mais) para determinar a intenção de sua nova versão, enquanto sua versão antiga é imediatamente clara. A legibilidade é um atributo importante do código, portanto, há um custo em sua nova versão.
Você está certo de que a nova versão evita uma divisão por zero. Certamente você não precisa adicionar uma guarda (na linha de
if (oldValue != 0)
). Mas isso faz sentido? Sua versão antiga reflete uma proporção entre dois números. Se o divisor é zero, sua proporção é indefinida. Isso pode ser mais significativo na sua situação, ie. você não deve produzir um resultado neste caso.A proteção contra estouro é discutível. Se você sabe que
newValue
é sempre maior queoldValue
, então talvez você possa argumentar. No entanto, pode haver casos em(oldValue * SOME_CONSTANT)
que também transbordará. Portanto, não vejo muito ganho aqui.Pode haver um argumento de que você obtém melhor desempenho porque a multiplicação pode ser mais rápida que a divisão (em alguns processadores). No entanto, teria que haver muitos cálculos como esses para obter um ganho significativo, ou seja. cuidado com a otimização prematura.
Refletindo sobre tudo o que foi exposto acima, em geral, acho que não há muito a ser ganho com sua nova versão em comparação com a versão antiga, principalmente devido à redução de clareza. No entanto, pode haver casos específicos em que há algum benefício.
fonte
Não.
Eu provavelmente chamaria isso de otimização prematura , em um sentido amplo, independentemente de você estar otimizando para o desempenho , como a frase geralmente se refere, ou qualquer outra coisa que possa ser otimizada, como contagem de bordas , linhas de código ou ainda mais amplamente, coisas como "design".
A implementação desse tipo de otimização como procedimento operacional padrão coloca em risco a semântica do seu código e potencialmente oculta as bordas. Os casos extremos que você deseja eliminar silenciosamente podem precisar ser explicitamente abordados de qualquer maneira . E é infinitamente mais fácil depurar problemas em bordas barulhentas (aquelas que lançam exceções) sobre aquelas que falham silenciosamente.
E, em alguns casos, é até vantajoso "des otimizar" por questões de legibilidade, clareza ou explícita. Na maioria dos casos, seus usuários não notarão que você salvou algumas linhas de código ou ciclos de CPU para evitar o tratamento de casos extremos ou o tratamento de exceções. Inábil ou silenciosamente falhar código, por outro lado, vai afetar as pessoas - seus colegas de trabalho, no mínimo. (E também, portanto, o custo para construir e manter o software.)
O padrão é o que for mais "natural" e legível em relação ao domínio do aplicativo e ao problema específico. Mantenha-o simples, explícito e idiomático. Otimize conforme necessário para ganhos significativos ou para atingir um limite de usabilidade legítimo.
Observe também: os compiladores geralmente otimizam a divisão para você de qualquer maneira - quando é seguro fazê-lo.
fonte
Use o que for menos buggy e faça mais sentido lógico.
Geralmente , a divisão por uma variável é uma má ideia, pois, geralmente, o divisor pode ser zero.
A divisão por uma constante geralmente depende apenas do significado lógico.
Aqui estão alguns exemplos para mostrar que isso depende da situação:
Divisão boa:
Multiplicação inválida:
Multiplicação boa:
Divisão ruim:
Multiplicação boa:
Divisão ruim:
fonte
(ptr2 - ptr1) * 3 >= n
tão fácil de entender quanto a expressãoptr2 - ptr1 >= n / 3
? Isso não faz seu cérebro tropeçar e voltar a tentar decifrar o significado de triplicar a diferença entre dois indicadores? Se é realmente óbvio para você e sua equipe, acho que tem mais poder; Eu devo apenas estar na minoria lenta.n
e um número arbitrário 3 são confusos nos dois casos, mas, substituídos por nomes razoáveis, não, não acho nem um mais confuso quanto o outro.Fazer qualquer coisa “sempre que possível” raramente é uma boa ideia.
Sua prioridade número um deve ser a correção, seguida pela legibilidade e manutenção. Substituir cegamente a divisão pela multiplicação, sempre que possível, geralmente falha no departamento de correção, às vezes apenas em casos raros e, portanto, difíceis de encontrar.
Faça o que é correto e mais legível. Se você tiver evidências sólidas de que escrever um código da maneira mais legível causa um problema de desempenho, considere alterá-lo. Cuidados, matemática e revisões de código são seus amigos.
fonte
Em relação à legibilidade do código, acho que a multiplicação é realmente mais legível em alguns casos. Por exemplo, se houver algo que você deve verificar se
newValue
aumentou 5% ou mais acimaoldValue
,1.05 * oldValue
existe um limite contra o qual testarnewValue
e é natural escreverMas tome cuidado com números negativos quando você refatorar as coisas dessa maneira (substituindo a divisão pela multiplicação ou substituindo a multiplicação pela divisão). As duas condições que você considerou são equivalentes se
oldValue
for garantido que não sejam negativas; mas suponha quenewValue
seja realmente -13,5 eoldValue
-10,1. Entãoavalia como verdadeiro , mas
avalia como falso .
fonte
Observe a famosa divisão de papel por números inteiros invariantes usando a multiplicação .
O compilador está realmente fazendo a multiplicação, se o número inteiro for invariável! Não é uma divisão. Isso acontece mesmo para não potência de 2 valores. O poder de 2 divisões usa obviamente mudanças de bits e, portanto, é ainda mais rápido.
No entanto, para números inteiros não invariantes, é sua responsabilidade otimizar o código. Antes de otimizar, verifique se você está realmente otimizando um gargalo genuíno e se a correção não é sacrificada. Cuidado com o excesso de número inteiro.
Eu me preocupo com a micro-otimização, então provavelmente daria uma olhada nas possibilidades de otimização.
Pense também nas arquiteturas em que seu código é executado. Especialmente o ARM possui uma divisão extremamente lenta; você precisa chamar uma função para dividir, não há instrução de divisão no ARM.
Além disso, em arquiteturas de 32 bits, a divisão de 64 bits não é otimizada, como descobri .
fonte
Pegando no seu ponto 2, ele realmente impedirá o estouro por um valor muito pequeno
oldValue
. No entanto, seSOME_CONSTANT
também for muito pequeno, seu método alternativo acabará com fluxo insuficiente, onde o valor não pode ser representado com precisão.E, inversamente, o que acontece se
oldValue
for muito grande? Você tem os mesmos problemas, exatamente o contrário.Se você deseja evitar (ou minimizar) o risco de transbordamento / transbordamento, a melhor maneira é verificar se o valor
newValue
é o mais próximooldValue
possívelSOME_CONSTANT
. Você pode então escolher a operação de divisão apropriada,ou
e o resultado será mais preciso.
Para dividir por zero, na minha experiência, quase nunca é apropriado ser "resolvido" na matemática. Se você tem uma divisão por zero em suas verificações contínuas, quase certamente você tem uma situação que requer alguma análise e quaisquer cálculos baseados nesses dados não fazem sentido. Uma verificação explícita de dividir por zero é quase sempre a movimentação apropriada. (Observe que eu digo "quase" aqui, porque não pretendo ser infalível. Vou apenas observar que não me lembro de ter visto uma boa razão para isso em 20 anos escrevendo software incorporado e segui em frente .)
No entanto, se você tiver um risco real de transbordamento / transbordamento em seu aplicativo, essa provavelmente não é a solução certa. O mais provável é que você geralmente verifique a estabilidade numérica do seu algoritmo, ou talvez simplesmente mude para uma representação de maior precisão.
E se você não tem um risco comprovado de estourar / estourar, não se preocupa com nada. Isso significa que você literalmente precisa provar que precisa, com números, nos comentários ao lado do código, que explicam ao mantenedor por que é necessário. Como engenheiro principal, revendo o código de outras pessoas, se eu encontrasse alguém que se esforçasse mais com isso, pessoalmente não aceitaria nada menos. Isso é exatamente o oposto da otimização prematura, mas geralmente teria a mesma causa raiz - obsessão por detalhes que não faz diferença funcional.
fonte
Encapsule a aritmética condicional em métodos e propriedades significativos. A boa nomeação não apenas diz o que "A / B" significa , como também a verificação de parâmetros e a manipulação de erros também podem ser ocultados.
Importante, como esses métodos são compostos em uma lógica mais complexa, a complexidade extrínseca permanece muito gerenciável.
Eu diria que a substituição da multiplicação parece uma solução razoável porque o problema está mal definido.
fonte
Eu acho que não seria uma boa ideia substituir multiplicações por divisões porque a ALU (Unidade Aritmética-Lógica) da CPU executa algoritmos, embora eles sejam implementados em hardware. Técnicas mais sofisticadas estão disponíveis nos processadores mais recentes. Geralmente, os processadores se esforçam para paralelizar as operações de pares de bits para minimizar os ciclos de clock necessários. Os algoritmos de multiplicação podem ser paralelizados de maneira bastante eficaz (embora sejam necessários mais transistores). Os algoritmos de divisão não podem ser paralelizados com a mesma eficiência. Os algoritmos de divisão mais eficientes são bastante complexos. Geralmente, eles exigem mais ciclos de clock por bit.
fonte