Chaque fois que j'ai besoin d'une division, par exemple d'une vérification de condition, je voudrais reformuler l'expression de la division en multiplication, par exemple:
Version originale:
if(newValue / oldValue >= SOME_CONSTANT)
Nouvelle version:
if(newValue >= oldValue * SOME_CONSTANT)
Parce que je pense que cela peut éviter:
Division par zéro
Débordement quand
oldValue
est très petit
Est-ce correct? Y at-il un problème pour cette habitude?
oldValue >= 0
?Réponses:
Deux cas courants à considérer:
Arithmétique entière
Évidemment, si vous utilisez une arithmétique entière (qui tronque), vous obtiendrez un résultat différent. Voici un petit exemple en C #:
Sortie:
Arithmétique en virgule flottante
Outre le fait que la division peut produire un résultat différent lorsqu'elle se divise par zéro (elle génère une exception, contrairement à la multiplication), elle peut également entraîner des erreurs d'arrondi légèrement différentes et un résultat différent. Exemple simple en C #:
Sortie:
Au cas où vous ne me croyez pas, voici un violon que vous pouvez exécuter et voir par vous-même.
D'autres langues peuvent être différentes. Gardez toutefois à l'esprit que C #, comme de nombreux langages, implémente une bibliothèque à virgule flottante au standard IEEE (IEEE 754) . Vous devriez donc obtenir les mêmes résultats dans d'autres temps d'exécution normalisés.
Conclusion
Si vous travaillez en greenfield , vous allez probablement bien.
Si vous travaillez sur du code hérité et que l'application est une application financière ou une autre application sensible qui effectue des calculs et est tenue de fournir des résultats cohérents, faites très attention lorsque vous modifiez des opérations. Si vous devez le faire, assurez-vous de disposer de tests unitaires capables de détecter d'éventuels changements subtils dans l'arithmétique.
Si vous ne faites que compter les éléments dans un tableau ou d’autres fonctions de calcul générales, tout ira bien. Je ne suis pas sûr que la méthode de multiplication rend votre code plus clair, cependant.
Si vous implémentez un algorithme dans une spécification, je ne changerais rien du tout, pas uniquement à cause du problème d'erreurs d'arrondi, mais pour que les développeurs puissent revoir le code et faire correspondre chaque expression à la spécification pour s'assurer qu'il n'y a pas d'implémentation. défauts.
la source
J'aime votre question car elle couvre potentiellement de nombreuses idées. Dans l’ensemble, je soupçonne que la réponse est cela dépend probablement des types impliqués et de la plage de valeurs possible dans votre cas particulier.
Mon instinct initial est de réfléchir à la style , c.-à-d. votre nouvelle version est moins claire pour le lecteur de votre code. J'imagine que je devrais réfléchir pendant une seconde ou deux (ou peut-être plus longtemps) pour déterminer l'intention de votre nouvelle version, alors que votre ancienne version est immédiatement claire. La lisibilité est un attribut important du code, votre nouvelle version a donc un coût.
Vous avez raison, la nouvelle version évite une division par zéro. Bien sûr, vous n’avez pas besoin d’ajouter une garde (dans le sens de
if (oldValue != 0)
). Mais est-ce que cela a du sens? Votre ancienne version reflète un rapport entre deux nombres. Si le diviseur est égal à zéro, votre ratio n'est pas défini. Cela peut être plus significatif dans votre situation, à savoir. vous ne devriez pas produire de résultat dans ce cas.La protection contre les débordements est discutable. Si vous savez qu'il
newValue
est toujours supérieur àoldValue
, alors vous pourriez peut-être présenter cet argument. Cependant, il peut y avoir des cas où(oldValue * SOME_CONSTANT)
débordera également. Donc, je ne vois pas beaucoup de gain ici.Il se peut que vous obteniez de meilleures performances car la multiplication peut être plus rapide que la division (sur certains processeurs). Cependant, de nombreux calculs tels que ceux-ci seraient nécessaires pour obtenir un gain significatif, à savoir. méfiez-vous de l'optimisation prématurée.
Si l’on réfléchit à tout ce qui précède, je pense qu’en général, il n’ya pas grand chose à gagner avec votre nouvelle version par rapport à l’ancienne version, en particulier compte tenu de la réduction de la clarté. Cependant, il peut y avoir des cas spécifiques où il y a un avantage.
la source
Non.
J'appellerais probablement cette optimisation prématurée , au sens large, que l'on optimise les performances , comme le dit l'expression, ou tout autre élément pouvant être optimisé, tel que le nombre de bord , les lignes de code ou encore plus largement, des choses comme "design".
L'implémentation de ce type d'optimisation en tant que procédure d'exploitation standard met en péril la sémantique de votre code et cache potentiellement les limites. Les cas marginaux que vous jugez utile d'éliminer en silence devront peut-être quand même être explicitement traités . Et, il est infiniment plus facile de résoudre les problèmes autour des contours bruyants (ceux qui jettent des exceptions) sur ceux qui échouent en silence.
Et, dans certains cas, il est même avantageux de "dé-optimiser" pour des raisons de lisibilité, de clarté ou d'explicite. Dans la plupart des cas, vos utilisateurs ne remarqueront pas que vous avez enregistré quelques lignes de code ou de cycles de processeur pour éviter la gestion des incidents et des exceptions. Code Maladroit ou à défaut en silence, d'autre part, va affecter les gens - vos collègues à tout le moins. (Et aussi, par conséquent, le coût de la construction et de la maintenance du logiciel.)
Par défaut, tout ce qui est plus "naturel" et lisible par rapport au domaine de l'application et au problème spécifique. Restez simple, explicite et idiomatique. Optimiser selon les besoins pour obtenir des gains importants ou pour atteindre un seuil d'utilisation légitime.
Notez également que les compilateurs optimisent souvent la division pour vous, quand vous pouvez le faire en toute sécurité .
la source
Utilisez celui qui est le moins buggé et qui a un sens plus logique.
En règle générale , la division par une variable est de toute façon une mauvaise idée, car le diviseur peut être égal à zéro.
La division par une constante dépend généralement de la signification logique.
Voici quelques exemples pour montrer que cela dépend de la situation:
Division bonne:
Multiplication mauvaise:
Multiplication bonne:
Division mauvaise:
Multiplication bonne:
Division mauvaise:
la source
(ptr2 - ptr1) * 3 >= n
aussi facile à comprendre que l'expressionptr2 - ptr1 >= n / 3
? Cela ne fait pas trébucher votre cerveau et vous relever en essayant de déchiffrer le sens de tripler la différence entre deux pointeurs? Si c'est vraiment évident pour vous et votre équipe, alors plus de pouvoir pour vous, je suppose; Je dois juste être dans la minorité lente.n
et un nombre arbitraire 3 sont déroutants dans les deux cas, mais remplacés par des noms raisonnables, non, je ne trouve pas que l'un soit plus déroutant que l'autre.Faire quelque chose «autant que possible» est très rarement une bonne idée.
Votre priorité numéro un devrait être l'exactitude, suivie de la lisibilité et de la maintenabilité. Remplacer aveuglément une division par une multiplication lorsque cela est possible échouera souvent dans le service de la correction, parfois seulement dans des cas rares et donc difficiles à trouver.
Faites ce qui est correct et le plus lisible. Si vous avez des preuves solides que l'écriture de code de la manière la plus lisible pose un problème de performances, vous pouvez alors le modifier. Les commentaires sur les soins, les maths et le code sont vos amis.
la source
En ce qui concerne la lisibilité du code, je pense que la multiplication est en réalité plus lisible dans certains cas. Par exemple, s'il y a quelque chose que vous devez vérifier si
newValue
a augmenté de 5% ou plusoldValue
, alors il1.05 * oldValue
y a un seuil à testernewValue
, et il est naturel d'écrireMais méfiez-vous des nombres négatifs lorsque vous refactorisez les choses de cette façon (en remplaçant la division par la multiplication ou en remplaçant la multiplication par la division). Les deux conditions que vous avez considérées sont équivalentes si la
oldValue
garantie n'est pas négative; mais supposenewValue
est en réalité -13,5 etoldValue
est -10,1. ensuiteévalue à vrai , mais
évalue à faux .
la source
Notez le fameux papier Division par Invariant Integers en utilisant la multiplication .
Le compilateur est en train de multiplier, si l'entier est invariant! Pas une division. Cela se produit même pour les valeurs non puissantes de 2. La puissance de 2 divisions utilise évidemment des bits de décalage et est donc encore plus rapide.
Cependant, pour les entiers non invariants, il vous incombe d'optimiser le code. Avant d’optimiser, assurez-vous d’optimiser réellement un véritable goulet d’étranglement et de ne pas sacrifier la correction. Attention au débordement d'entier.
La micro-optimisation me tient à cœur, donc je jetterais probablement un coup d'œil aux possibilités d'optimisation.
Pensez également aux architectures sur lesquelles votre code est exécuté. Surtout ARM a une division extrêmement lente; vous devez appeler une fonction pour diviser, il n’ya pas d’instruction de division dans ARM.
De plus, sur les architectures 32 bits, la division 64 bits n'est pas optimisée, comme je l'ai découvert .
la source
En reprenant votre point 2, cela empêchera effectivement le débordement pour un très petit
oldValue
. Toutefois, siSOME_CONSTANT
est également très petite, votre méthode alternative se retrouvera avec un dépassement inférieur, où la valeur ne peut pas être représentée avec précision.Et inversement, que se passe-t-il si
oldValue
est très grand? Vous avez les mêmes problèmes, à l’inverse.Si vous souhaitez éviter (ou minimiser) le risque de débordement / dépassement, le meilleur moyen consiste à vérifier si l'
newValue
ampleur est la plus proche deoldValue
ou la plus procheSOME_CONSTANT
. Vous pouvez ensuite choisir l’opération de division appropriée, soitou
et le résultat sera le plus précis.
Pour ce qui est de la division par zéro, selon mon expérience, il n’est presque jamais approprié d’être "résolu" en maths. Si vos contrôles continus se divisent par zéro, vous avez certainement une situation qui nécessite une analyse et tout calcul fondé sur ces données n'a pas de sens. Une vérification explicite de la division par zéro est presque toujours le mouvement approprié. (Notez que je dis «presque» ici, parce que je ne prétends pas être infaillible. Je noterai simplement que je ne me souviens pas avoir vu une bonne raison à cela en 20 ans d'écriture de logiciels embarqués, et je passe à autre chose. .)
Toutefois, si votre application présente un risque réel de débordement / de débordement, ce n'est probablement pas la bonne solution. Plus probablement, vous devriez généralement vérifier la stabilité numérique de votre algorithme, ou peut-être simplement passer à une représentation plus précise.
Et si vous ne possédez pas de risque avéré de débordement / de sous-remplissage, vous ne craignez plus rien. Cela signifie que vous devez littéralement prouver que vous en avez besoin, avec des chiffres, des commentaires à côté du code qui expliquent à un responsable pourquoi c'est nécessaire. En tant qu'ingénieur principal en train de réviser le code d'autres personnes, si je rencontrais quelqu'un qui prenait des efforts supplémentaires, je n'accepterais personnellement rien de moins. C’est un peu l’opposé de l’optimisation prématurée, mais elle aurait généralement la même cause fondamentale: l’obsession des détails qui ne fait aucune différence fonctionnelle.
la source
Encapsulez l'arithmétique conditionnelle dans des méthodes et des propriétés significatives. Non seulement une bonne dénomination vous dira ce que "A / B" signifie , la vérification des paramètres et la gestion des erreurs peuvent également s'y cacher.
Il est important de noter que ces méthodes étant composées d'une logique plus complexe, la complexité extrinsèque reste très gérable.
Je dirais que la substitution par multiplication semble une solution raisonnable car le problème est mal défini.
la source
Je pense que ce ne serait pas une bonne idée de remplacer les multiplications par des divisions, car l’ALU (unité arithmétique-logique) du processeur exécute des algorithmes, même s’ils sont implémentés dans le matériel. Des techniques plus sophistiquées sont disponibles dans les nouveaux processeurs. Généralement, les processeurs s'efforcent de paralléliser les opérations de paires de bits afin de minimiser les cycles d'horloge requis. Les algorithmes de multiplication peuvent être parallélisés assez efficacement (bien que davantage de transistors soient nécessaires). Les algorithmes de division ne peuvent pas être parallélisés aussi efficacement. Les algorithmes de division les plus efficaces sont assez complexes. Généralement, ils nécessitent plus de cycles d'horloge par bit.
la source