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


73

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

元のバージョン:

if(newValue / oldValue >= SOME_CONSTANT)

新しいバージョン:

if(newValue >= oldValue * SOME_CONSTANT)

回避できると思うから:

  1. ゼロ除算

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

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


41
負の数では、2つのバージョンが完全に異なるものをチェックすることに注意してください。確かですoldValue >= 0か?
user2313067

37
言語によっては(しかし、最も注目すべきはCで)、あなたが考えることができるものは何でも、最適化、コンパイラは通常、より良いそれを行うことができ、-OR-は、全くそれをしないための十分な意味を持っています。
マークベニングフィールド

63
XとYが意味的に同等でない場合、常にコードXをコードYに置き換えることは決して「良い習慣」ではありません。しかし、常に XとYを見て、頭をオンにし、要件が何であるかを考えてから、2つの選択肢のうちどちらが正しいかを判断することをお勧めします。その後、セマンティックの違いが正しいことを確認するために必要なテストについても検討する必要があります。
Doc Brown

12
@MarkBenningfield:何であれ、コンパイラはゼロ除算を最適化できません。あなたが考えている「最適化」は「速度の最適化」です。OPは、別の種類の最適化-バグ回避について考えています。
スリーブマン

25
ポイント2は偽物です。元のバージョンは小さな値に対してオーバーフローする可能性がありますが、新しいバージョンは大きな値に対してオーバーフローする可能性があるため、どちらも一般的なケースでは安全ではありません。
ジャックB

回答:


74

考慮すべき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で作業している場合は、おそらく大丈夫です。

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

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

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


41
第二の財政的ビット。この種のスイッチは、会計士が熊手であなたを追いかけていることを求めています。私は5,000行を覚えています。「正しい」答えを見つけるよりも、ピッチフォークを寄せ付けないようにするためにより多くの努力をしなければなりませんでした。.01%オフであっても問題ありません。絶対に一貫した回答が必須でした。したがって、体系的な丸め誤差を引き起こすような方法で計算する必要がありました。
ローレンペクテル

8
5セントのキャンディーを購入することを考えてください(もう存在しません)。
ローレンペクテル

24
@LorenPechtel、ほとんどの税制には、トランザクションごとに課税されるルール(明白な理由により)が含まれており、税は領域の最小コイン以上の増分で課税されるため、小額は納税者に有利なように切り捨てられるためです。これらのルールは合法で一貫しているため、「正しい」ものです。熊手を持つ会計士は、コンピュータープログラマーが経験していない方法で、ルールが実際に何であるかをおそらく知っています(経験豊富な会計士でない限り)。0.01%のエラーはバランスエラーの原因になる可能性が高く、バランスエラーがあることは違法です。
スティーブ

9
グリーンフィールドという言葉を聞いたことがないので、調べました。ウィキペディアによると、「事前の作業によって課せられる制約のないプロジェクト」です。
ヘンリック・リッパ

9
@Steve:私の上司は最近、「グリーンフィールド」と「ブラウンフィールド」を対比しました。特定のプロジェクトは「ブラックフィールド」に似ていることに気付きました...
-D

25

あなたの質問は多くのアイデアをカバーする可能性があるので気に入っています。全体として、答えは、おそらく関連するタイプと特定のケースで可能な値の範囲に依存するということだと思います。

私の最初の本能は、スタイルを反映することです。新しいバージョンは、コードの読者にはあまりわかりません。古いバージョンはすぐに明確になるのに対し、新しいバージョンの意図を判断するには、1〜2秒(またはそれ以上)考えなければならないでしょう。可読性はコードの重要な属性であるため、新しいバージョンにはコストがかかります。

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

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

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

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


16
ええと、実世界のマシンでは、任意の除算よりも効率的な任意の乗算は、実際にはプロセッサに依存しません。
デュプリケータ

1
整数対浮動小数点演算の問題もあります。比率が分数の場合、除算は浮動小数点で実行する必要があり、キャストが必要です。キャストを見逃すと、意図しない間違いが発生します。分数が2つの小さな整数の比率である場合、それらを再配置すると、整数演算で比較を行うことができます。(この時点で引数が適用されます。)
rwong

@rwong常にではありません。いくつかの言語では、小数部分を削除して整数除算を行うため、キャストは必要ありません。
T.サール-モニカ元に戻し

@ T.Sarあなたが説明するテクニックと答えで説明されるセマンティクスは異なります。意味論とは、プログラマが答えを浮動小数点値にするか小数値にするかということです。ここで説明する手法は、逆数乗算による除算です。これは、整数除算の完全な近似(置換)である場合があります。後者の手法は、整数の逆数(2 ** 32シフト)の導出がコンパイル時に行われるため、除数が事前にわかっている場合に通常適用されます。実行時にそれを行うことは、CPUをより高価にするため、有益ではありません。
-rwong

22

番号。

フレーズが一般的に言及しているように、パフォーマンスのために最適化しているかどうかにかかわらず、広い意味で、その早期最適化を呼び出すと思います、またはエッジカウントコードの行、またはさらに広く言えば、「デザイン」のようなものです。

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

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

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

また、注意:コンパイラは、とにかく除算最適化することがよくあります(安全な場合)。


11
-1この回答は、分割の潜在的な落とし穴に関する質問に実際には当てはまりません-最適化とは関係ありません
ベンコットレル

13
@BenCottrellそれは完璧にフィットします。落とし穴は、保守性を犠牲にして無意味なパフォーマンス最適化に価値を置くことにあります。「この習慣に問題はありますか?」という質問から - はい。それはすぐに絶対的な意味不明な文章を書くことにつながります。
マイケル

9
@Michaelは、これらのことについても質問していません-具体的には、それぞれが異なるセマンティクスと動作を持っているが、両方が同じ要件を満たすように意図されている2つの異なる式の正確さについて尋ねています。
ベンコットレル

5
@BenCottrellおそらく、質問のどこで正確性について言及されているのか教えていただけますか?
マイケル

5
@BenCottrellあなたは「できません」と言ったはずです:)
マイケル

13

バグが少なく、より論理的な意味を持つ方を使用してください。

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

以下に、状況に応じて表示する例をいくつか示します。

部門良い:

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

2
文脈に依存することに同意しますが、最初の2組の例は非常に貧弱です。どちらの場合でも、私はどちらか一方を好まないでしょう。
マイケル

6
@マイケル:うーん...あなた(ptr2 - ptr1) * 3 >= nは式と同じように理解しやすいことがわかりptr2 - ptr1 >= n / 3ますか?それはあなたの脳をトリップさせて、2つのポインターの違いを3倍にする意味を解読しようとして戻って来ませんか?あなたとあなたのチームにとってそれが本当に明白なものであるなら、あなたにより多くの力があると思います。私はただゆっくりした少数派でなければなりません。
Mehrdad

2
呼び出された変数とn任意の数字3は、どちらの場合も混乱を招きますが、合理的な名前に置き換えて、どちらかがもう一方よりも混乱しているとは思いません。
マイケル

1
これらの例は、本当に貧弱ではありません。間違いなく「極端に貧弱」ではありません-「合理的な名前」に潜んでいても、悪い場合に交換しても意味がありません。私がプロジェクトを初めて使用する場合、本番コードを修正するために行ったときに、この回答にリストされている「良い」ケースを見るとよいでしょう。
ジョンM

3

やって何もすることは「可能な限り」めったに良いアイデアではありません。

最優先事項は正確性であり、次に読みやすさと保守性が優先されます。可能な場合はいつでも除算を乗算で盲目的に置き換えると、正確性部門で失敗することがよくあります。

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


1

可読性のコードを、私は乗算が実際だと思うより、いくつかのケースで読めます。たとえば、newValue5%以上増加したかどうかを確認する必要があるものがある場合oldValue、それ1.05 * oldValueはテスト対象のしきい値でありnewValue、記述するのが自然です

    if (newValue >= 1.05 * oldValue)

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

newValue/oldValue >= 1.05

trueと評価されますが、

newValue >= 1.05 * oldValue

falseと評価されます


1

乗算を使用した不変整数による有名な論文Divisionに注意してください。

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

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

私はマイクロ最適化に関心があるので、おそらく最適化の可能性を検討します。

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

私としても、32ビットアーキテクチャ上で、64ビットの分割は、最適化されていないが分かりました


1

ポイント2を取り上げると、非常に小さいのオーバーフローを実際に防ぐことができoldValueます。ただし、SOME_CONSTANTも非常に小さい場合、代替方法はアンダーフローになり、値を正確に表すことができません。

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

オーバーフロー/アンダーフローのリスクを回避(または最小化)したい場合、最良の方法は、にnewValue最も近いかどうかを確認するoldValueことSOME_CONSTANTです。その後、適切な除算操作を選択できます。

    if(newValue / oldValue >= SOME_CONSTANT)

または

    if(newValue / SOME_CONSTANT >= oldValue)

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

ゼロ除算の場合、私の経験では、これは数学で「解決」することはほとんど適切ではありません。連続チェックでゼロ除算を行っている場合、ほぼ確実に何らかの分析が必要な状況があり、このデータに基づく計算は無意味です。明示的なゼロ除算チェックは、ほとんどの場合適切な動きです。(私はここで「ほぼ」と言っていることに注意してください。なぜなら、私は間違いがないと主張していないからです。 )

ただし、アプリケーションでオーバーフロー/アンダーフローの本当のリスクがある場合、これはおそらく適切なソリューションではありません。より可能性が高いのは、一般的にアルゴリズムの数値安定性を確認するか、単純に高精度の表現に移行することです。

また、オーバーフロー/アンダーフローのリスクが証明されていない場合は、何も心配していません。つまり、必要な理由をメンテナーに説明するコードの隣のコメントに、数字で、文字通り必要であることを証明する必要があることを意味します。他の人のコードをレビューするプリンシパルエンジニアとして、これに余分な労力を費やしている人に出くわした場合、私は個人的にそれ以下のものを受け入れません。これは時期尚早な最適化の反対のようなものですが、一般的には同じ根本的な原因があります-機能的な違いをもたらさない詳細への執着。


0

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

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

問題が明確に定義されていないため、乗算代入は合理的な解決策と思われます。


0

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.