每当需要除法(例如条件检查)时,我都希望将除法的表达式重构为乘法,例如:
原始版本:
if(newValue / oldValue >= SOME_CONSTANT)
新版本:
if(newValue >= oldValue * SOME_CONSTANT)
因为我认为它可以避免:
被零除
oldValue
很小的时候溢出
那正确吗?这个习惯有问题吗?
每当需要除法(例如条件检查)时,我都希望将除法的表达式重构为乘法,例如:
原始版本:
if(newValue / oldValue >= SOME_CONSTANT)
新版本:
if(newValue >= oldValue * SOME_CONSTANT)
因为我认为它可以避免:
被零除
oldValue
很小的时候溢出
那正确吗?这个习惯有问题吗?
Answers:
需要考虑两种常见情况:
显然,如果您使用整数算术(将其截断),则会得到不同的结果。这是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)浮点库,因此您应该在其他标准化运行时获得相同的结果。
如果您正在绿地工作,则可能还可以。
如果您正在使用旧代码,并且该应用程序是执行算术并需要提供一致结果的财务或其他敏感应用程序,则在更改操作时要格外小心。如果必须,请确保您具有单元测试,该单元测试将检测算术中的任何细微变化。
如果您只是在做一些事情,例如对数组中的元素计数或其他通用计算函数,那么您可能会没事的。不过,我不确定乘法方法会使您的代码更清晰。
如果您要根据规范实现算法,那么我将不做任何更改,这不仅是因为舍入误差的问题,而且开发人员可以查看代码并将每个表达式映射回规范以确保没有实现缺陷。
我喜欢您的问题,因为它可能涉及很多想法。总的来说,我怀疑答案取决于它,可能取决于所涉及的类型以及您特定情况下的值的可能范围。
我最初的本能是反思风格,即。您的代码的读者不太清楚您的新版本。我想我将需要花一两秒钟(或更长的时间)来确定您的新版本的意图,而您的旧版本会立即变得清晰。可读性是代码的重要属性,因此新版本会产生成本。
您是正确的,新版本避免被零除。当然,您无需添加保护(沿的行if (oldValue != 0)
)。但这有意义吗?您的旧版本反映了两个数字之间的比率。如果除数为零,则您的比率不确定。这在您的情况下可能更有意义。在这种情况下,您不应产生任何结果。
防止溢出的措施值得商bat。如果您知道该newValue
值始终大于oldValue
,则可以提出该论点。但是,在某些情况下(oldValue * SOME_CONSTANT)
也会溢出。因此,我在这里看不到太多收获。
可能有一个论点是您可以获得更好的性能,因为在某些处理器上乘法可能比除法更快。但是,为此必须进行许多这样的计算才能获得显着收益。提防过早的优化。
综上所述,总的来说,我认为与旧版本相比,新版本没有太多收获,特别是考虑到清晰度的降低。但是,在某些情况下可能会有一些好处。
没有。
广义上讲,我可能会称其为过早优化,无论您是针对性能(通常是指短语)进行优化,还是可以进行优化的其他任何事物(例如edge-count,代码行或更广泛地说,是“设计”之类的东西。
将这种优化作为标准操作过程来实现,会使代码的语义面临风险,并可能隐藏边缘。无论如何,可能需要显式解决您认为适合静默消除的边缘情况。而且,相对于那些静默失败的问题,调试在嘈杂的边缘(引发异常的边缘)上的问题绝对容易得多。
而且,在某些情况下,出于可读性,清晰度或明确性的考虑,“取消优化”甚至更为有利。在大多数情况下,您的用户不会注意到您已经保存了几行代码或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
吗?它不会使您的大脑绊倒并试图尝试破译使两个指针之间的差值增加三倍的含义吗?如果对您和您的团队确实很明显,那么我想您将拥有更多的能力;我一定只是少数族裔。
n
在这两种情况下,一个称为的变量和一个任意数字3都令人困惑,但是用合理的名称代替,没有,我发现这两个变量都比另一个更令人困惑。
“尽可能” 做任何事情很少是一个好主意。
您的第一要务是正确性,其次是可读性和可维护性。只要有可能,用乘法盲目地除以除法通常会在正确性部门失败,有时仅在极少数情况下,因此很难找到案例。
做正确和最易读的事情。如果有确凿的证据表明以最易读的方式编写代码会导致性能问题,则可以考虑对其进行更改。护理,数学和代码评论是您的朋友。
关于代码的可读性,我认为在某些情况下乘法实际上更具可读性。例如,如果您必须检查某物是否newValue
增加了5%或更多oldValue
,则1.05 * oldValue
这是要测试的阈值newValue
,自然可以编写
if (newValue >= 1.05 * oldValue)
但是,当您以这种方式重构事物时,请当心负数(要么用乘法替换除法,或者用除法替换乘法)。如果oldValue
保证不为负,则您考虑的两个条件是相等的;但假设newValue
实际上是-13.5,oldValue
是-10.1。然后
newValue/oldValue >= 1.05
评估为true,但
newValue >= 1.05 * oldValue
评估为false。
注意著名的论文使用不变乘除不变整数。
如果整数是不变的,则编译器实际上正在执行乘法!没有分裂。即使对于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)
结果将是最准确的。
对于零除,以我的经验,这几乎永远都不适合在数学中“解决”。如果您在连续检查中被零除,那么几乎可以肯定,您需要进行一些分析,并且基于此数据进行的任何计算都是没有意义的。明确的除零检查几乎总是适当的举动。(请注意,我在这里确实说“几乎”,因为我并没有说它是绝对可靠的。我只是要指出,我不记得在编写嵌入式软件的20年中看到过这样做的充分理由,而是继续前进。 )
但是,如果您的应用程序确实有上溢/下溢的风险,那么这可能不是正确的解决方案。通常,您通常应该检查算法的数值稳定性,或者可能只是简单地转向精度更高的表示形式。
而且,如果您没有经过证明的上溢/下溢风险,那么您就不必担心。这确实意味着您确实需要在代码旁边的注释中用数字证明您需要它,并向维护人员解释为什么有必要。作为审查其他人代码的首席工程师,如果我遇到其他需要为此付出额外努力的人,那么我个人将不会接受其他任何内容。这与过早优化相反,但通常会产生相同的根本原因-对细节的痴迷不会造成功能上的差异。
我认为用除法代替乘法不是一个好主意,因为CPU的ALU(算术逻辑单元)执行算法,尽管它们是在硬件中实现的。较新的处理器中提供了更复杂的技术。通常,处理器努力使位对操作并行化,以最小化所需的时钟周期。乘法算法可以非常有效地并行化(尽管需要更多的晶体管)。除法算法无法高效并行化。最有效的除法算法非常复杂。通常,它们每位需要更多的时钟周期。
oldValue >= 0
吗?