在可能的情况下,用除法代替除法是一种好习惯吗?


73

每当需要除法(例如条件检查)时,我都希望将除法的表达式重构为乘法,例如:

原始版本:

if(newValue / oldValue >= SOME_CONSTANT)

新版本:

if(newValue >= oldValue * SOME_CONSTANT)

因为我认为它可以避免:

  1. 被零除

  2. oldValue很小的时候溢出

那正确吗?这个习惯有问题吗?


41
请注意,如果使用负数,这两个版本将检查完全不同的事物。你确定oldValue >= 0吗?
user2313067

37
取决于语言(但最值得注意的是使用C语言),无论您想到什么优化,编译器通常都可以做得更好,-OR-具有足够的意识以至于根本不这样做。
Mark Benningfield '18

63
当X和Y在语义上不相等时,永远不要用代码Y替换代码X,这绝不是“好习惯” 。但是查看X和Y,打开大脑,考虑需求是什么,然后决定这两种选择中的哪一种更正确,始终是一个好主意。之后,您还应该考虑需要进行哪些测试才能验证您正确理解了语义差异。
布朗

12
@MarkBenningfield:没什么,编译器无法优化除零。您正在考虑的“优化”是“速度优化”。OP正在考虑另一种优化-避免错误。
slebetman '18

25
点2是伪造的。原始版本可能会溢出以获取较小的值,但新版本可能会溢出以获取较大的值,因此在一般情况下,这两种方法都不安全。
JacquesB

Answers:


74

需要考虑两种常见情况:

整数运算

显然,如果您使用整数算术(将其截断),则会得到不同的结果。这是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)浮点库,因此您应该在其他标准化运行时获得相同的结果。

结论

如果您正在绿地工作,则可能还可以。

如果您正在使用旧代码,并且该应用程序是执行算术并需要提供一致结果的财务或其他敏感应用程序,则在更改操作时要格外小心。如果必须,请确保您具有单元测试,该单元测试将检测算术中的任何细微变化。

如果您只是在做一些事情,例如对数组中的元素计数或其他通用计算函数,那么您可能会没事的。不过,我不确定乘法方​​法会使您的代码更清晰。

如果您要根据规范实现算法,那么我将不做任何更改,这不仅是因为舍入误差的问题,而且开发人员可以查看代码并将每个表达式映射回规范以确保没有实现缺陷。


41
其次是财务方面。这种开关要求会计师用干草叉追你。我记得有5,000行,我不得不花更多的精力来保持干草叉,而不是找到“正确”的答案-实际上,这实际上是稍微错误的。偏离0.01%没关系,绝对一致的答案是强制性的。因此,我被迫进行计算,导致系统的舍入误差。
罗伦·佩希特尔

8
考虑购买5美分的糖果(现在已经不存在了。)购买20片,“正确”的答案是没有税,因为购买20件每片都免税。
罗伦·佩希特尔

24
@LorenPechtel,这是因为大多数税收系统都包含一个规则(出于明显的原因),即每笔交易都要征税,并且应以不小于该领域最小硬币的增量递增税收,并且小数部分应四舍五入以纳税人的意愿。这些规则是“正确的”,因为它们是合法且一致的。有干草叉的会计师可能以计算机程序员不会知道的方式知道规则的真正含义(除非他们也是有经验的会计师)。0.01%的误差可能会导致平衡误差,并且具有平衡误差是非法的。
史蒂夫(Steve)

9
因为我之前从未听说过“未开发的土地 ”一词,所以我进行了查找。维基百科说这是“一个没有先前工作所施加的任何约束的项目”。
亨里克·里帕

9
@Steve:我的老板最近将“未开发的地区”与“棕色的地区”进行了对比。我指出某些项目更像是“黑场” ... :-D
DevSolar

25

我喜欢您的问题,因为它可能涉及很多想法。总的来说,我怀疑答案取决于它,可能取决于所涉及的类型以及您特定情况下的值的可能范围。

我最初的本能是反思风格,即。您的代码的读者不太清楚您的新版本。我想我将需要花一两秒钟(或更长的时间)来确定您的新版本的意图,而您的旧版本会立即变得清晰。可读性是代码的重要属性,因此新版本会产生成本。

您是正确的,新版本避免被零除。当然,您无需添加保护(沿的行if (oldValue != 0))。但这有意义吗?您的旧版本反映了两个数字之间的比率。如果除数为零,则您的比率不确定。这在您的情况下可能更有意义。在这种情况下,您不应产生任何结果。

防止溢出的措施值得商bat。如果您知道该newValue值始终大于oldValue,则可以提出该论点。但是,在某些情况下(oldValue * SOME_CONSTANT)也会溢出。因此,我在这里看不到太多收获。

可能有一个论点是您可以获得更好的性能,因为在某些处理器上乘法可能比除法更快。但是,为此必须进行许多这样的计算才能获得显着收益。提防过早的优化。

综上所述,总的来说,我认为与旧版本相比,新版本没有太多收获,特别是考虑到清晰度的降低。但是,在某些情况下可能会有一些好处。


16
嗯,对于现实世界的机器来说,任意乘法比任意除法更有效,这实际上与处理器无关。
Deduplicator

1
还有整数与浮点运算的问题。如果比率是分数,则除法需要在浮点数中进行,需要强制转换。缺少演员表将导致意外的错误。如果该分数恰好是两个小整数之间的比率,那么重新排列它们就可以用整数算术进行比较。(此时您的论点将适用。)
rwong

@rwong并非总是如此。几种语言通过删除小数部分来进行整数除法,因此不需要强制转换。
T. Sar

@ T.Sar您描述的技术和答案中描述的语义是不同的。语义是程序员希望答案是浮点数还是小数;您描述的技术是倒数相除,对于整数除法,有时这是一个完美的近似(替代)。后一种技术通常在事先知道除数时应用,因为整数倒数(偏移2 ** 32)可以在编译时完成。在运行时执行此操作不会有任何好处,因为它会占用更多CPU资源。
rwong

22

没有。

广义上讲,我可能会称其为过早优化,无论您是针对性能(通常是指短语)进行优化,还是可以进行优化的其他任何事物(例如edge-count代码行或更广泛地说,是“设计”之类的东西

将这种优化作为标准操作过程来实现,会使代码的语义面临风险,并可能隐藏边缘。无论如何,可能需要显式解决您认为适合静默消除的边缘情况。而且,相对于那些静默失败的问题,调试在嘈杂的边缘(引发异常的边缘)上的问题绝对容易得多。

而且,在某些情况下,出于可读性,清晰度或明确性的考虑,“取消优化”甚至更为有利。在大多数情况下,您的用户不会注意到您已经保存了几行代码或CPU周期,以避免出现极端情况处理或异常处理。另一方面,笨拙或无声的代码失败影响人们-至少影响您的同事。(因此,还有构建和维护软件的成本。)

对于应用程序的域和特定问题,默认为更“自然”的和可读的。保持简单,明确和惯用。为获得重大收益或达到合法的可用性阈值而进行必要的优化。

另外请注意:编译器通常优化师为你啦-当它是安全的这样做。


11
-1这个答案真的不适合这个问题,这是关于除法的潜在陷阱–与优化无关
Ben Cottrell

13
@BenCottrell非常合适。陷阱在于以可维护性为代价在无意义的性能优化中增加价值。从“这个习惯有问题吗?”这个问题开始 -是的 它将很快导致书写绝对乱码。
迈克尔

9
@Michael,问题也不是问这些事情中的任何一个,而是要问两个不同的表达式的正确性,这两个表达式的语义和行为各不相同,但都旨在满足相同的要求。
Ben Cottrell '18年

5
@BenCottrell也许您可以向我指出,在问题中何处提及正确性?
迈克尔

5
@BenCottrell您应该说'我不能':)
Michael

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
我同意这取决于上下文,但是您的前两对示例非常糟糕。在任何一种情况下,我都不会偏爱一个。
迈克尔

6
@迈克尔:嗯...你发现(ptr2 - ptr1) * 3 >= n它和表达一样容易理解ptr2 - ptr1 >= n / 3吗?它不会使您的大脑绊倒并试图尝试破译使两个指针之间的差值增加三倍的含义吗?如果对您和您的团队确实很明显,那么我想您将拥有更多的能力;我一定只是少数族裔。
Mehrdad

2
n在这两种情况下,一个称为的变量和一个任意数字3都令人困惑,但是用合理的名称代替,没有,我发现这两个变量都比另一个更令人困惑。
迈克尔

1
这些例子并不是真的很穷。.绝对不是“极度贫困”-即使您使用“合理的名称”,当您将它们换为坏情况时,它们的意义仍然较小。如果我是一个项目的新手,我会在修复一些生产代码时看到此答案中列出的“好”案例。
John-M

3

“尽可能” 做任何事情很少是一个好主意。

您的第一要务是正确性,其次是可读性和可维护性。只要有可能,用乘法盲目地除以除法通常会在正确性部门失败,有时仅在极少数情况下,因此很难找到案例。

做正确和最易读的事情。如果有确凿的证据表明以最易读的方式编写代码会导致性能问题,则可以考虑对其进行更改。护理,数学和代码评论是您的朋友。


1

关于代码的可读性,我认为在某些情况下乘法实际上更具可读性。例如,如果您必须检查某物是否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


1

注意著名的论文使用不变乘除不变整数

如果整数是不变的,则编译器实际上正在执行乘法!没有分裂。即使对于2值的非幂,也会发生这种情况。2进制的幂显然使用位移,因此甚至更快。

但是,对于非不变整数,您有责任优化代码。在进行优化之前,请确保您确实在优化真正的瓶颈,并且没有牺牲正确性。当心整数溢出。

我关心微优化,所以我可能会看看优化的可能性。

还考虑代码在其上运行的体系结构。特别是ARM的划分非常慢;您需要调用一个函数进行除法,ARM中没有除法指令。

此外,在32位体系结构,64位除法没有被优化,因为我发现了


1

接您的观点2,确实会在很小的程度上防止溢出oldValue。但是,如果SOME_CONSTANT值也很小,则您的替代方法将导致下溢,在该情况下无法准确表示该值。

相反,如果oldValue很大,会发生什么?您有同样的问题,正好相反。

如果你想避免(或减少)溢/下溢的风险,最好的办法是检查是否newValue是最接近的幅度oldValueSOME_CONSTANT。然后,您可以选择适当的除法运算

    if(newValue / oldValue >= SOME_CONSTANT)

要么

    if(newValue / SOME_CONSTANT >= oldValue)

结果将是最准确的。

对于零除,以我的经验,这几乎永远都不适合在数学中“解决”。如果您在连续检查中被零除,那么几乎可以肯定,您需要进行一些分析,并且基于此数据进行的任何计算都是没有意义的。明确的除零检查几乎总是适当的举动。(请注意,我在这里确实说“几乎”,因为我并没有说它是绝对可靠的。我只是要指出,我不记得在编写嵌入式软件的20年中看到过这样做的充分理由,而是继续前进。 )

但是,如果您的应用程序确实有上溢/下溢的风险,那么这可能不是正确的解决方案。通常,您通常应该检查算法的数值稳定性,或者可能只是简单地转向精度更高的表示形式。

而且,如果您没有经过证明的上溢/下溢风险,那么您就不必担心。这确实意味着您确实需要在代码旁边的注释中用数字证明您需要它,并向维护人员解释为什么有必要。作为审查其他人代码的首席工程师,如果我遇到其他需要为此付出额外努力的人,那么我个人将不会接受其他任何内容。这与过早优化相反,但通常会产生相同的根本原因-对细节的痴迷不会造成功能上的差异。


0

将条件算法封装在有意义的方法和属性中。好的命名不仅会告诉您“ A / B”的含义,参数检查和错误处理也可以很好地隐藏在其中。

重要的是,由于这些方法由更复杂的逻辑组成,因此外部复杂性仍然非常易于管理。

我会说乘法替换似乎是一个合理的解决方案,因为该问题定义不明确。


0

我认为用除法代替乘法不是一个好主意,因为CPU的ALU(算术逻辑单元)执行算法,尽管它们是在硬件中实现的。较新的处理器中提供了更复杂的技术。通常,处理器努力使位对操作并行化,以最小化所需的时钟周期。乘法算法可以非常有效地并行化(尽管需要更多的晶体管)。除法算法无法高效并行化。最有效的除法算法非常复杂。通常,它们每位需要更多的时钟周期。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.