web-dev-qa-db-ja.com

可能な場合、除算を乗算で置き換えるのは良い習慣ですか?

条件チェックなどの除算が必要なときはいつでも、除算の式を乗算にリファクタリングしたいと思います。たとえば、次のようにします。

元のバージョン:

if(newValue / oldValue >= SOME_CONSTANT)

新しいバージョン:

if(newValue >= oldValue * SOME_CONSTANT)

それは回避できると思うので:

  1. ゼロ除算

  2. oldValueが非常に小さい場合のオーバーフロー

そうですか?この習慣に問題はありますか?

72
ocomfd

考慮すべき2つの一般的なケース:

整数演算

整数演算(切り捨て)を使用している場合は、明らかに異なる結果が得られます。 C#の小さな例を次に示します。

public static void TestIntegerArithmetic()
{
    int newValue = 101;
    int oldValue = 10;
    int SOME_CONSTANT = 10;

    if(newValue / oldValue > SOME_CONSTANT)
    {
        Console.WriteLine("First comparison says it's bigger.");
    }
    else
    {
        Console.WriteLine("First comparison says it's not bigger.");
    }

    if(newValue > oldValue * SOME_CONSTANT)
    {
        Console.WriteLine("Second comparison says it's bigger.");
    }
    else
    {
        Console.WriteLine("Second comparison says it's not bigger.");
    }
}

出力:

First comparison says it's not bigger.
Second comparison says it's bigger.

浮動小数点演算

除算は、ゼロで除算すると異なる結果を生成する可能性があるという事実(例外は生成されますが、乗算は生成されません)は別として、丸め誤差がわずかに異なり、結果が異なる場合があります。 C#での簡単な例:

public static void TestFloatingPoint()
{
    double newValue = 1;
    double oldValue = 3;
    double SOME_CONSTANT = 0.33333333333333335;

    if(newValue / oldValue >= SOME_CONSTANT)
    {
        Console.WriteLine("First comparison says it's bigger.");
    }
    else
    {
        Console.WriteLine("First comparison says it's not bigger.");
    }

    if(newValue >= oldValue * SOME_CONSTANT)
    {
        Console.WriteLine("Second comparison says it's bigger.");
    }
    else
    {
        Console.WriteLine("Second comparison says it's not bigger.");
    }
}

出力:

First comparison says it's not bigger.
Second comparison says it's bigger.

あなたが私を信じていない場合のために、- これはフィドルです 実行して自分で見ることができます。

他の言語は異なる場合があります。ただし、C#は多くの言語と同様に IEEE標準(IEEE 754) 浮動小数点ライブラリを実装しているため、他の標準化されたランタイムでも同じ結果が得られることを覚えておいてください。

結論

greenfield で作業している場合は、おそらく大丈夫です。

レガシーコードで作業していて、アプリケーションが財務計算またはその他の機密性の高いアプリケーションであり、算術を実行し、一貫した結果を提供する必要がある場合は、操作を変更するときに非常に注意してください。必要な場合は、算術の微妙な変更を検出する単体テストがあることを確認してください。

配列やその他の一般的な計算関数の要素を数えるようなことをしているだけなら、おそらく大丈夫でしょう。ただし、乗算方法によってコードがより明確になるかどうかはわかりません。

アルゴリズムを仕様に実装している場合、丸めエラーの問題のためだけでなく、開発者がコードを確認して各式を仕様にマップして実装がないことを確認できるように、何も変更しません。欠陥。

73
John Wu

あなたの質問には多くのアイデアが含まれている可能性があるため、気に入っています。概して、私はおそらくそれは依存するであると思います。おそらく、関係する型と特定のケースで可能な値の範囲にあります。

私の最初の本能はstyleを反映することです。あなたの新しいバージョンはあなたのコードの読者にとってあまり明確ではありません。新しいバージョンの意図を判断するには、1、2(またはそれ以上)長く考えなければならないのではないかと思いますが、古いバージョンはすぐにわかります。読みやすさはコードの重要な属性であるため、新しいバージョンではコストがかかります。

新しいバージョンはゼロによる除算を回避するのはあなたの言うとおりです。確かに、(if (oldValue != 0)の行に沿って)ガードを追加する必要はありません。しかし、これは理にかなっていますか?古いバージョンは2つの数値の比率を反映しています。除数がゼロの場合、比率は未定義です。これはあなたの状況でより意味があるかもしれません。この場合、結果を生成するべきではありません。

オーバーフローに対する保護は議論の余地があります。 newValueが常にoldValueよりも大きいことがわかっている場合は、おそらくその引数を作成できます。ただし、(oldValue * SOME_CONSTANT)もオーバーフローします。したがって、ここではあまり利益が見られません。

乗算は除算よりも高速になる可能性があるため、一部のプロセッサではパフォーマンスが向上するという議論があるかもしれません。ただし、これを行うには、このような多くの計算を行う必要があります。時期尚早の最適化に注意してください。

上記のすべてを反映して、一般的に、明確さの低下を考えると、古いバージョンと比較して、新しいバージョンで得られるものは多くないと思います。ただし、いくつかの利点がある特定の場合があります。

25
dave

いいえ

パフォーマンスを最適化しているかどうかに関係なく、一般的には 時期尚早の最適化 と呼びますが、このフレーズは一般的に、または Edge-countcode lines などの最適化できるもの、またはさらに広くは "design。" のようなもの

この種の最適化を標準操作手順として実装すると、コードのセマンティクスが危険にさらされ、エッジが非表示になる可能性があります。静かに排除するのに適していると思われるEdgeケースは、明示的に対処する必要がある場合がありますとにかく。また、静かに失敗するものよりも、ノイズの多いエッジ(例外をスローするもの)の周りの問題をデバッグする方がはるかに簡単です。

また、場合によっては、読みやすさ、明確さ、または明示性のために、「最適化を解除する」ことも有利です。ほとんどの場合、エッジケース処理や例外処理を回避するために数行のコードやCPUサイクルを節約したことにユーザーは気付かないでしょう。一方、ぎこちない、または静かに失敗するコードは、は人に影響を及ぼします-少なくとも同僚に影響します。 (したがって、ソフトウェアの構築と保守にかかるコストも。)

デフォルトは、アプリケーションのドメインと特定の問題に関して、より「自然」で読みやすいものです。シンプルで、明示的で、慣用的です。 重要なゲインのために、または正当なユーザビリティしきい値を達成するために必要に応じて最適化します。

また注意してください:コンパイラ 最適化除算 とにかくあなたのために-それがsafe そうするには。

22
svidgen

バグが少なく、より論理的な意味のある方を使用します。

通常、変数による除算はとにかく悪い考えです。通常、除数はゼロになる可能性があるためです。
通常、定数による除算は、論理的な意味が何であるかに依存します。

状況によって異なることを示す例を次に示します。

良い部門:

if ((ptr2 - ptr1) >= n / 3)  // good: check if length of subarray is at least n/3
    ...

乗算が悪い:

if ((ptr2 - ptr1) * 3 >= n)  // bad: confusing!! what is the intention of this code?
    ...

乗算が良い:

if (j - i >= 2 * min_length)  // good: obviously checking for a minimum length
    ...

悪い部門:

if ((j - i) / 2 >= min_length)  // bad: confusing!! what is the intention of this code?
    ...

乗算が良い:

if (new_length >= old_length * 1.5)  // good: is the new size at least 50% bigger?
    ...

悪い部門:

if (new_length / old_length >= 2)  // bad: BUGGY!! will fail if old_length = 0!
    ...
13
user541686

anything「可能な限り」を行うことは、非常にまれです。

あなたの最優先事項は、正確性であり、その後に可読性と保守性が続きます。可能な限り盲目的に除算を乗算に置き換えると、正確性部門で失敗することが多く、まれにしか発生しないため、ケースを見つけるのが困難になります。

正しい、最も読みやすいものを実行します。最も読みやすい方法でコードを作成するとパフォーマンスの問題が発生するという確かな証拠がある場合は、コードを変更することを検討できます。ケア、数学、コードレビューはあなたの友達です。

3
gnasher729

コードの読みやすさについては、乗算は実際にはだと思いますもっと場合によっては読み取り可能です。たとえば、newValueoldValueを5%以上超えているかどうかを確認する必要がある場合、1.05 * oldValuenewValueをテストするしきい値であり、次のように書くのが自然です

    if (newValue >= 1.05 * oldValue)

しかし負の数に注意このようにリファクタリングする場合(除算を乗算で置き換えるか、乗算を除算で置き換える)。 oldValueが負でないことが保証されている場合、検討した2つの条件は同等です。ただし、newValueが実際には-13.5で、oldValueが-10.1であるとします。その後

newValue/oldValue >= 1.05

trueと評価されますが、

newValue >= 1.05 * oldValue

falseと評価されます。

1
David K

有名な論文 Division by Invariant Integers by Multiplication に注意してください。

整数が不変である場合、コンパイラは実際に乗算を実行しています!部門ではありません。これは、2のべき乗でない値でも発生します。 2の累乗の除算は明らかにビットシフトを使用するため、さらに高速です。

ただし、不変の整数の場合、コードを最適化するのはユーザーの責任です。最適化する前に、本物のボトルネックを本当に最適化していること、およびその正確さが犠牲にならないことを確認してください。整数オーバーフローに注意してください。

私はミクロの最適化に関心があるので、おそらく最適化の可能性を見ていきます。

コードを実行するアーキテクチャについても検討してください。特にARMは除算が非常に遅いため、除算するには関数を呼び出す必要があります。ARMには除算命令がありません。

また、32ビットアーキテクチャでは、I found out となるため、64ビット除算は最適化されません。

1
juhist

ポイント2に到達すると、非常に小さいoldValueのオーバーフローが実際に防止されます。ただし、SOME_CONSTANTも非常に小さい場合、別の方法ではアンダーフローが発生し、値を正確に表現できなくなります。

逆に、oldValueが非常に大きい場合はどうなりますか?あなたは同じ問題を抱えていますが、正反対の方法です。

オーバーフロー/アンダーフローのリスクを回避(または最小限に抑える)したい場合は、newValueoldValueまたはSOME_CONSTANTに最も近いかどうかを確認するのが最善の方法です。次に、適切な除算演算を選択できます。

    if(newValue / oldValue >= SOME_CONSTANT)

または

    if(newValue / SOME_CONSTANT >= oldValue)

結果は最も正確になります。

ゼロ除算の場合、私の経験では、これを数学で「解決」することはほとんど適切ではありません。継続的なチェックでゼロ除算がある場合、ほぼ確実に、何らかの分析が必要な状況にあり、このデータに基づく計算は無意味です。ほとんどの場合、明示的なゼロ除算チェックが適切な方法です。 (私はここで「ほぼ」と言っていることに注意してください。私は間違いがないと主張していないからです。組み込みソフトウェアを書いてから20年でこれについての正当な理由を見た覚えがないので、次に進みます。 。)

ただし、アプリケーションで実際にオーバーフロー/アンダーフローが発生するリスクがある場合、これはおそらく正しい解決策ではありません。通常、アルゴリズムの数値的な安定性を確認するか、単純に高精度の表現に移行する必要があります。

また、オーバーフロー/アンダーフローのリスクが証明されていない場合は、何も心配していません。それはあなたが文字通りが必要であることを証明する必要があることを意味します。コードの隣のコメントで、それがなぜ必要なのかをメンテナに説明します。他の人のコードをレビューする主任エンジニアとして、私がこれに余分な努力をしている誰かに出会った場合、私は個人的にはそれ以上のものを受け入れません。これは時期尚早の最適化の逆のようなものですが、それは一般的に同じ根本原因があります-機能的な違いを作らない詳細への執着。

1
Graham

CPUのALU(算術論理演算ユニット)はハードウェアで実装されていますが、アルゴリズムを実行するため、乗算を除算に置き換えるのは良い考えではないと思います。新しいプロセッサでは、より高度な手法が利用できます。一般に、プロセッサは、必要なクロックサイクルを最小限に抑えるために、ビットペアの操作を並列化しようとします。乗算アルゴリズムは非常に効果的に並列化できます(ただし、より多くのトランジスタが必要です)。除算アルゴリズムを効率的に並列化することはできません。最も効率的な除算アルゴリズムは非常に複雑です。一般に、ビットあたりのクロックサイクルが多く必要です。

0
Ishan Shah

意味のあるメソッドとプロパティに条件付き算術をカプセル化します。 「A/B」手段が適切な名前でわかるだけでなく、パラメータのチェックとエラー処理もそこにきちんと隠すことができます。

重要なことに、これらのメソッドはより複雑なロジックに構成されているため、extrinsicの複雑さは非常に扱いやすいままです。

問題が明確に定義されていないため、乗算置換は妥当な解決策のように思えます。

0
radarbob