条件チェックなどの除算が必要なときはいつでも、除算の式を乗算にリファクタリングしたいと思います。たとえば:
元のバージョン:
if(newValue / oldValue >= SOME_CONSTANT)
新しいバージョン:
if(newValue >= oldValue * SOME_CONSTANT)
回避できると思うから:
ゼロ除算
オーバーフロー
oldValue
が非常に小さい場合
そうですか?この習慣に問題はありますか?
条件チェックなどの除算が必要なときはいつでも、除算の式を乗算にリファクタリングしたいと思います。たとえば:
元のバージョン:
if(newValue / oldValue >= SOME_CONSTANT)
新しいバージョン:
if(newValue >= oldValue * SOME_CONSTANT)
回避できると思うから:
ゼロ除算
オーバーフローoldValue
が非常に小さい場合
そうですか?この習慣に問題はありますか?
回答:
考慮すべき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で作業している場合は、おそらく大丈夫です。
レガシーコードで作業しており、アプリケーションが算術を実行し、一貫した結果を提供する必要がある金融その他の機密性の高いアプリケーションである場合、操作を変更するときは非常に注意してください。必要な場合は、算術演算の微妙な変更を検出する単体テストがあることを確認してください。
配列やその他の一般的な計算関数の要素を数えるようなことをしているだけなら、おそらく大丈夫でしょう。ただし、乗算方法によってコードがより明確になるかどうかはわかりません。
アルゴリズムを仕様に実装する場合、丸めエラーの問題だけでなく、開発者がコードを確認し、各式を仕様にマッピングして実装がないことを確認できるようにするため、何も変更しません欠陥。
あなたの質問は多くのアイデアをカバーする可能性があるので気に入っています。全体として、答えは、おそらく関連するタイプと特定のケースで可能な値の範囲に依存するということだと思います。
私の最初の本能は、スタイルを反映することです。新しいバージョンは、コードの読者にはあまりわかりません。古いバージョンはすぐに明確になるのに対し、新しいバージョンの意図を判断するには、1〜2秒(またはそれ以上)考えなければならないでしょう。可読性はコードの重要な属性であるため、新しいバージョンにはコストがかかります。
あなたは、新しいバージョンがゼロによる除算を避けることは正しいです。確かに(の行に沿ってif (oldValue != 0)
)ガードを追加する必要はありません。しかし、これは理にかなっていますか?古いバージョンは、2つの数値の比率を反映しています。除数がゼロの場合、比率は定義されていません。これはあなたの状況でより意味があるかもしれません。この場合、結果を生成しないでください。
オーバーフローに対する保護は議論の余地があります。それnewValue
が常により大きいことを知っているならoldValue
、おそらくあなたはその議論をすることができます。ただし、(oldValue * SOME_CONSTANT)
オーバーフローする場合もあります。したがって、ここではあまり利益が得られません。
(一部のプロセッサでは)乗算は除算よりも高速であるため、パフォーマンスが向上するという議論があるかもしれません。ただし、このためにこれらのような多くの計算が必要になります。時期尚早の最適化に注意してください。
上記のすべてを考慮すると、一般に、特に明確さの低下を考えると、古いバージョンと比較して、新しいバージョンで得られるものはあまりないと思います。ただし、何らかの利点がある特定の場合があります。
番号。
フレーズが一般的に言及しているように、パフォーマンスのために最適化しているかどうかにかかわらず、広い意味で、その早期最適化を呼び出すと思います、またはエッジカウント、コードの行、またはさらに広く言えば、「デザイン」のようなものです。
この種の最適化を標準の操作手順として実装すると、コードのセマンティクスが危険にさらされ、潜在的にエッジが隠れてしまいます。静かに削除するのに適していると思われるエッジケースは、とにかく明示的に対処する必要があります。また、ノイズの多いエッジ(例外をスローするエッジ)の周囲の問題を、静かに失敗するものよりもはるかに簡単にデバッグできます。
また、場合によっては、読みやすさ、明確さ、または明示性のために「最適化を解除」することも有利です。ほとんどの場合、ユーザーは、エッジケース処理または例外処理を回避するために数行のコードまたはCPUサイクルを保存したことに気付かないでしょう。一方、厄介なコードや静かに失敗するコードは、人々に影響を与えます。少なくとも同僚です。(また、したがって、ソフトウェアを構築および保守するためのコスト。)
アプリケーションのドメインおよび特定の問題に関して、より「自然」で読みやすいものにデフォルト設定します。シンプル、明示的、慣用的にしてください。大きな利益を得るため、または正当なユーザビリティのしきい値を達成するために、必要に応じて最適化します。
通常、除数はゼロになる可能性があるため、変数による除算はとにかく悪い考えです。
定数による除算は通常、論理的な意味に依存します。
以下に、状況に応じて表示する例をいくつか示します。
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!
...
(ptr2 - ptr1) * 3 >= n
は式と同じように理解しやすいことがわかりptr2 - ptr1 >= n / 3
ますか?それはあなたの脳をトリップさせて、2つのポインターの違いを3倍にする意味を解読しようとして戻って来ませんか?あなたとあなたのチームにとってそれが本当に明白なものであるなら、あなたにより多くの力があると思います。私はただゆっくりした少数派でなければなりません。
n
任意の数字3は、どちらの場合も混乱を招きますが、合理的な名前に置き換えて、どちらかがもう一方よりも混乱しているとは思いません。
やって何もすることは「可能な限り」めったに良いアイデアではありません。
最優先事項は正確性であり、次に読みやすさと保守性が優先されます。可能な場合はいつでも除算を乗算で盲目的に置き換えると、正確性部門で失敗することがよくあります。
正しく、最も読みやすいものを実行してください。最も読みやすい方法でコードを記述するとパフォーマンスの問題が発生するという確固たる証拠がある場合は、変更することを検討できます。ケア、数学、コードレビューは友達です。
可読性のコードを、私は乗算が実際だと思うより、いくつかのケースで読めます。たとえば、newValue
5%以上増加したかどうかを確認する必要があるものがある場合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と評価されます。
乗算を使用した不変整数による有名な論文Divisionに注意してください。
整数が不変の場合、コンパイラは実際に乗算を実行しています!部門ではありません。これは、2のべき乗以外の値でも発生します。2の累乗の除算は明らかにビットシフトを使用するため、さらに高速です。
ただし、非不変整数の場合、コードを最適化するのはユーザーの責任です。最適化する前に、本物のボトルネックを本当に最適化していること、そしてその正確性が犠牲になっていないことを確認してください。整数オーバーフローに注意してください。
私はマイクロ最適化に関心があるので、おそらく最適化の可能性を検討します。
コードを実行するアーキテクチャについても考えてください。特にARMの除算は非常に遅いです。除算する関数を呼び出す必要があります。ARMには除算命令はありません。
私としても、32ビットアーキテクチャ上で、64ビットの分割は、最適化されていないが分かりました。
ポイント2を取り上げると、非常に小さいのオーバーフローを実際に防ぐことができoldValue
ます。ただし、SOME_CONSTANT
も非常に小さい場合、代替方法はアンダーフローになり、値を正確に表すことができません。
逆に、oldValue
非常に大きい場合はどうなりますか?あなたは同じ問題を抱えており、ちょうど逆の方法です。
オーバーフロー/アンダーフローのリスクを回避(または最小化)したい場合、最良の方法は、にnewValue
最も近いかどうかを確認するoldValue
ことSOME_CONSTANT
です。その後、適切な除算操作を選択できます。
if(newValue / oldValue >= SOME_CONSTANT)
または
if(newValue / SOME_CONSTANT >= oldValue)
結果は最も正確になります。
ゼロ除算の場合、私の経験では、これは数学で「解決」することはほとんど適切ではありません。連続チェックでゼロ除算を行っている場合、ほぼ確実に何らかの分析が必要な状況があり、このデータに基づく計算は無意味です。明示的なゼロ除算チェックは、ほとんどの場合適切な動きです。(私はここで「ほぼ」と言っていることに注意してください。なぜなら、私は間違いがないと主張していないからです。 )
ただし、アプリケーションでオーバーフロー/アンダーフローの本当のリスクがある場合、これはおそらく適切なソリューションではありません。より可能性が高いのは、一般的にアルゴリズムの数値安定性を確認するか、単純に高精度の表現に移行することです。
また、オーバーフロー/アンダーフローのリスクが証明されていない場合は、何も心配していません。つまり、必要な理由をメンテナーに説明するコードの隣のコメントに、数字で、文字通り必要であることを証明する必要があることを意味します。他の人のコードをレビューするプリンシパルエンジニアとして、これに余分な労力を費やしている人に出くわした場合、私は個人的にそれ以下のものを受け入れません。これは時期尚早な最適化の反対のようなものですが、一般的には同じ根本的な原因があります-機能的な違いをもたらさない詳細への執着。
CPUのALU(算術論理ユニット)はアルゴリズムを実行するため、乗算を除算に置き換えることは良い考えではないと思いますが、アルゴリズムはハードウェアで実装されています。新しいプロセッサでは、より洗練された手法を利用できます。一般に、プロセッサは、必要なクロックサイクルを最小限に抑えるために、ビットペア操作を並列化しようとします。乗算アルゴリズムは非常に効果的に並列化できます(ただし、より多くのトランジスタが必要です)。除算アルゴリズムを効率的に並列化することはできません。最も効率的な除算アルゴリズムは非常に複雑です。一般的に、ビットあたりのクロックサイクルが多く必要です。
oldValue >= 0
か?