我应该使用乘法还是除法?


118

这是一个愚蠢的有趣问题:

假设我们必须执行一个简单的操作,其中我们需要变量值的一半。有通常这样做的方法有两种:

y = x / 2.0;
// or...
y = x * 0.5;

假设我们使用的是语言随附的标准运算符,那么哪一种具有更好的性能?

我猜想乘法通常更好,所以我在编码时会尽量坚持下去,但我想确认一下。

尽管我个人对Python 2.4-2.5 的答案感兴趣,但是也可以发布其他语言的答案!而且,如果您愿意,也可以发布其他更奇特的方式(例如使用按位移位运算符)。


5
您是否进行了基准测试?大约只有十几行代码。您从运行基准测试中学到了什么?[提示:这样做比在这里发布问题要快。]
S.Lott

4
伟大的问题,已经引起了一些非常有趣的答案/讨论。谢谢:)
Stealthcopter 2011年

22
即使他已经通过基准测试了解了答案,它仍然是一个有用的问题,并且已经产生了一些有趣且有用的答案。我也希望人们坚持这一点,避免对答案写评论和评论,就是否值得进行优化而给出不相关的建议。为什么不假设OP在按书面形式询问问题,而不是假设他或她“确实”想要大规模重写建议。
凯文·怀特富特

1
除法比乘法慢得多。但是某些智能编译器/虚拟机将除法转换为乘法,因此您的测试将具有相同的结果(两个测试均测试乘法)。
伊万·库基尔

4
话题有点离题,但我只想说说我对@KevinWhitefoot的认同。没有什么比阅读讲道者更令人沮丧的,而不是对技术问题的技术性回答。感谢Kevin的评论!
让·弗朗索瓦·

Answers:


78

蟒蛇:

time python -c 'for i in xrange(int(1e8)): t=12341234234.234 / 2.0'
real    0m26.676s
user    0m25.154s
sys     0m0.076s

time python -c 'for i in xrange(int(1e8)): t=12341234234.234 * 0.5'
real    0m17.932s
user    0m16.481s
sys     0m0.048s

乘法快33%

卢阿:

time lua -e 'for i=1,1e8 do t=12341234234.234 / 2.0 end'
real    0m7.956s
user    0m7.332s
sys     0m0.032s

time lua -e 'for i=1,1e8 do t=12341234234.234 * 0.5 end'
real    0m7.997s
user    0m7.516s
sys     0m0.036s

=>没有真正的区别

LuaJIT:

time luajit -O -e 'for i=1,1e8 do t=12341234234.234 / 2.0 end'
real    0m1.921s
user    0m1.668s
sys     0m0.004s

time luajit -O -e 'for i=1,1e8 do t=12341234234.234 * 0.5 end'
real    0m1.843s
user    0m1.676s
sys     0m0.000s

=>仅快5%

结论:在Python中,乘法要快于除法,但是当您使用更高级的VM或JIT接近CPU时,优势就消失了。将来的Python VM很可能会使其变得无关紧要


感谢您提供使用time命令进行基准测试的技巧!
Edmundito

2
您的结论是错误的。随着JIT / VM变得更好,它变得越来越重要。与VM的较低开销相比,该划分变得更慢。请记住,为了保证精度,编译器通常不能对浮点进行太多优化。
拉斯姆斯2012年

7
@rasmus:随着JIT的改进,即使您要求除法,也更有可能使用CPU乘法指令。
Ben Voigt

68

始终使用最清晰的东西。您所做的任何其他操作都试图使编译器的性能超越智能。如果编译器是完全智能的,它将尽最大努力优化结果,但是没有什么可以使下一个家伙不讨厌您讨厌的笨拙的位移解决方案(顺便说一句,我喜欢位操作,很有趣。但是很有趣!=可读) )

过早的优化是万恶之源。永远记住优化的三个规则!

  1. 不要优化。
  2. 如果您是专家,请参阅规则1
  3. 如果您是专家并且可以证明需要,那么请使用以下过程:

    • 对其进行未优化的编码
    • 确定“足够快”的速度-注意哪个用户要求/故事需要该指标。
    • 编写速度测试
    • 测试现有代码-如果速度足够快,就可以完成。
    • 重新编码优化
    • 测试优化的代码。如果不符合指标,则将其丢弃并保留原始指标。
    • 如果符合测试要求,请保留原始代码作为注释

同样,执行诸如在不需要时删除内部循环或为插入排序选择数组上的链表之类的操作也不是优化,而只是编程。


7
那不是Knuth的全部报价;看到en.wikipedia.org/wiki/...
贾森小号

不,来自许多不同来源的主题大约有40种不同的引用。我有点凑在一起。
比尔K

您的最后一句话不清楚何时应用规则#1和#2,从而使我们回到了开始的地方:我们需要确定哪些优化值得,哪些优化不值得。假装答案很明显不是答案。
马特

2
真的让您感到困惑吗?除非您实际上不符合客户端规范并且对整个系统非常熟悉,包括CPU的语言和缓存特性,否则请始终应用规则1和2。那时,仅遵循3中的步骤,不要只是想:“嘿,如果我在本地缓存此变量而不是调用getter,事情可能会更快。首先证明它不够快,然后分别测试每个优化并扔掉那些没有帮助的那些文件重沿途所有。
比尔ķ

49

我认为这变得太挑剔了,您最好做任何使代码更具可读性的事情。除非您执行数千次(甚至数百万次)操作,否则我怀疑有人会注意到这种差异。

如果您真的必须做出选择,则基准测试是唯一的选择。查找哪些功能给您带来了问题,然后找出功能中出现问题的位置,然后修复这些部分。但是,我仍然怀疑单个数学运算(甚至重复多次,多次)是否会引起任何瓶颈。


1
当我过去制造雷达处理器时,一次操作确实有所作为。但是,我们正在手动优化机器代码以实现实时性能。对于其他所有事情,我都赞成简单明了。
S.Lott

我猜对于某些事情,您可能只关心一个操作。但是我希望在99%的应用程序中都没关系。
Thomas Owens

27
特别是因为OP在Python中寻找答案。我怀疑任何需要达到这种效率的东西都会用Python编写。
Ed S.

4
在三角相交例程中,除法可能是最昂贵的操作,这是大多数光线跟踪器的基础。如果存储倒数并乘而不是除,则将经历很多倍的加速。
宽容

@solinent-是的,可以提速,但我怀疑“很多次”-浮点数除法和乘法的差异不应超过4:1,除非所讨论的处理器确实针对乘法而不是除法进行了优化。
杰森·S

39

乘法更快,除法更准确。如果您的数字不是2的幂,则将失去一些精度。

y = x / 3.0;
y = x * 0.333333;  // how many 3's should there be, and how will the compiler round?

即使让编译器找出倒数常量以达到最佳精度,答案也可能有所不同。

x = 100.0;
x / 3.0 == x * (1.0/3.0)  // is false in the test I just performed

速度问题仅可能在C / C ++或JIT语言中才有关系,甚至在操作陷入瓶颈时也是如此。


如果您要除以整数,则除法是准确的。
底座

7
分母>分子的浮点除法必须在低阶位中引入无意义的值;除法通常会降低准确性。
S.Lott

8
@ S.Lott:不,那不是真的。所有符合IEEE-754的浮点实现必须相对于当前舍入模式将每个运算的结果完美舍入(即,舍入到最接近的浮点数)。乘以倒数总是会引入更多的误差,至少是因为必须再进行一次舍入。
Electro

1
我知道这个答案已有8年历史了,但它具有误导性。您可以执行除法而不会显着降低精度:y = x * (1.0/3.0);并且编译器通常会在编译时计算1/3。是的,1/3是不完全可表示在IEEE-754,但是当你执行浮点运算你失去精度反正,不管你是在做乘法或除法,因为低位是圆形的。如果您知道您的计算对舍入误差非常敏感,那么您还应该知道如何最好地处理该问题。
詹森·S

1
@JasonS我刚刚让程序运行了一整夜,从1.0开始并以1 ULP递增;我将乘以(1.0/3.0)除以的结果进行了比较3.0。我的分数高达1.0000036666774155,在那个空间中7.3%的结果有所不同。我认为它们之间只有1位的差异,但是由于保证IEEE算术可以舍入到最接近的正确结果,因此我坚持说除法更准确。差异是否显着取决于您。
Mark Ransom

25

如果您想优化代码,但仍然很清楚,请尝试以下操作:

y = x * (1.0 / 2.0);

编译器应该能够在编译时进行除法,因此您可以在运行时进行乘法。我希望精度与y = x / 2.0情况相同。

LOT可能在嵌入式处理器中需要浮点仿真来计算浮点算术,这点很重要。


12
适合您自己(以及为此而选择-1的任何人)-这是嵌入式领域的标准做法,该领域的软件工程师认为这很清楚。
詹森·S

4
+1是唯一的实现者,意识到编译器无法根据需要优化浮点运算。他们甚至不能为了保证精度而改变操作数的顺序(除非它使用宽松模式)。
拉斯姆斯2012年

1
OMG,至少有6位程序员认为基本数学尚不清楚。AFAIK,IEEE 754乘法是可交换的(但不相关)。
maaartinus 2014年

13
也许您错过了重点。它与代数正确性无关。在理想的世界中,您应该只能将其除以二:y = x / 2.0;,但是在现实世界中,您可能不得不让编译器哄骗执行更便宜的乘法。也许还不清楚为什么y = x * (1.0 / 2.0);更好,而陈述它会更清楚y = x * 0.5;。但是将其更改2.0为a 7.0,我宁愿看到而y = x * (1.0 / 7.0);不是y = x * 0.142857142857;
詹森·S

3
这确实很清楚为什么使用您的方法更清晰(更精确)。
Juan Martinez

21

只是要为“其他语言”选项添加一些内容。
C:由于这只是一个学术活动是真的没有任何区别,所以我想我会有所作为。

我没有进行任何优化就编译为汇编,并查看了结果。
代码:

int main() {

    volatile int a;
    volatile int b;

    asm("## 5/2\n");
    a = 5;
    a = a / 2;

    asm("## 5*0.5");
    b = 5;
    b = b * 0.5;

    asm("## done");

    return a + b;

}

用编译 gcc tdiv.c -O1 -o tdiv.s -S

除以2:

movl    $5, -4(%ebp)
movl    -4(%ebp), %eax
movl    %eax, %edx
shrl    $31, %edx
addl    %edx, %eax
sarl    %eax
movl    %eax, -4(%ebp)

乘以0.5:

movl    $5, -8(%ebp)
movl    -8(%ebp), %eax
pushl   %eax
fildl   (%esp)
leal    4(%esp), %esp
fmuls   LC0
fnstcw  -10(%ebp)
movzwl  -10(%ebp), %eax
orw $3072, %ax
movw    %ax, -12(%ebp)
fldcw   -12(%ebp)
fistpl  -16(%ebp)
fldcw   -10(%ebp)
movl    -16(%ebp), %eax
movl    %eax, -8(%ebp)

但是,当我将ints 更改为doubles(python可能会这样做)时,我得到了:

师:

flds    LC0
fstl    -8(%ebp)
fldl    -8(%ebp)
flds    LC1
fmul    %st, %st(1)
fxch    %st(1)
fstpl   -8(%ebp)
fxch    %st(1)

乘法:

fstpl   -16(%ebp)
fldl    -16(%ebp)
fmulp   %st, %st(1)
fstpl   -16(%ebp)

我没有对任何代码进行基准测试,但是仅通过检查代码就可以看到,使用整数,除以2的时间比乘以2的时间短。使用双精度,乘法的时间更短,因为编译器使用处理器的浮点操作码,即可能比不使用它们执行相同的操作要快(但实际上我不知道)。因此,最终的答案表明,乘以0.5与除以2的性能取决于语言的实现及其运行的平台。最终,差异几乎可以忽略不计,除了可读性以外,您几乎永远不必担心。

作为附带说明,您可以在我的程序中看到main()return a + b。当我拿掉volatile关键字时,您将永远不会猜测程序集的外观(不包括程序设置):

## 5/2

## 5*0.5
## done

movl    $5, %eax
leave
ret

它在一条指令中完成了除法,乘法和加法运算!显然,如果优化程序是任何受人尊敬的,您都不必为此担心。

抱歉,答案太长了。


1
这不是一个“单一指令”。它只是不断折叠。
kvanberendonck

5
@kvanberendonck当然,这是一条指令。计算一下:movl $5, %eax 优化的名称并不重要,甚至不相关。您只是想屈服于四年的答案。
卡森·迈尔斯,

2
优化的性质仍然很重要,因为它是上下文相关的:它仅在要添加/相乘/除法/等时才适用。编译时常量,编译器可以提前完成所有数学运算,并在运行时将最终答案移入寄存器。在一般情况下(运行时间除数),除法要比乘法慢很多,但是我想乘以倒数仅在以其他方式除以相同的分母多次时才有用。您可能知道所有这些,但是较新的程序员可能需要详细说明,以防万一。
Mike S

10

首先,除非您使用C或ASSEMBLY进行工作,否则您可能使用的是高级语言,其中内存停滞和常规调用开销将使相乘和相除之间的差异完全相形见to。因此,只要选择在这种情况下更好的方法即可。

如果您是从很高的级别进行交谈,那么您可能会用它来测量任何事情的速度都不会明显变慢。您还会在其他答案中看到,人们需要做一百万次乘/除运算,才能测量两者之间的亚毫秒级差异。

如果您仍然好奇,请从低级优化的角度来看:

除法往往具有比乘积更长的流水线。这意味着获得结果要花费更长的时间,但是如果您可以让处理器忙于执行非相关任务,那么最终花费的成本不会超过乘法。

流水线差异有多长完全取决于硬件。我使用的最后一个硬件是FPU乘法的9个周期和FPU除法的50个周期。听起来很多,但随后您会因内存不足而丢失1000个周期,因此可以将其视为现实。

打个比方,就是在看电视节目时把馅饼放在微波炉里。将您带离电视节目的总时间是将其放入微波炉中并从微波炉中取出要花费多长时间。剩下的时间,您仍然看电视节目。因此,如果该饼花了10分钟而不是1分钟来煮,那么它实际上并没有用光电视观看时间。

在实践中,如果您要关注乘法和除法之间的差异,则需要了解管道,缓存,分支停顿,无序预测和管道依赖性。如果这听起来不像您打算解决的问题,那么正确的答案就是忽略两者之间的区别。

许多(许多)年前,绝对重要的是要避免使用分隔符并始终使用乘法,但是那时候记忆命中的意义不大,而分隔符则更为糟糕。这些天来,我对可读性的评价更高,但是如果没有可读性差异,我认为选择乘数是一个好习惯。



6

做你需要的一切。首先考虑您的读者,在确定性能问题之前,不要担心性能。

让编译器为您完成性能。


5

如果您使用整数或非浮点类型,请不要忘记您的移位运算符:<< >>

    int y = 10;
    y = y >> 1;
    Console.WriteLine("value halved: " + y);
    y = y << 1;
    Console.WriteLine("now value doubled: " + y);

7
这种优化是在任何现代编译器中自动进行的。
达斯汀·盖兹

有没有人测试过是否检查(使用位操作)操作数(?)是否具有可转换的版本来代替?函数mul(a,b){如果(b为2)返回<< 如果(b是4)返回<< <<;// ...等返回a * b; 我的猜测是,IF太昂贵了,效率会降低。
Christopher Lightfoot,

那没有印出我想像的任何地方。没关系。
Christopher Lightfoot,

对于const操作,普通的编译器应该可以完成;但是这里我们使用的是python,所以我不确定它是否足够聪明?(它应该是)。
Christopher Lightfoot,

捷径不错,只是无法立即弄清实际情况。大多数程序员甚至都不认识位移运算符。
Blazemonger,2011年

4

实际上,有充分的理由认为,作为一般经验法则,乘法将比除法更快。硬件中的浮点除法是通过移位和条件减法算法(带有二进制数的“长除法”)完成的,或者(最近很可能是)通过类似于Goldschmidt算法的迭代完成。移位和减法每位精度至少需要一个周期(几乎不可能像乘法一样进行迭代并行化),并且迭代算法至少要进行一次乘法每次迭代。无论哪种情况,该部门极有可能需要更多的周期。当然,这不考虑编译器中的怪癖,数据移动或精度。但是,总的来说,如果要在程序的时间敏感部分中编码一个内部循环,那么编写0.5 * x1.0/2.0 * x而不是x / 2.0这样做是合理的。“编写最清晰的代码”的学步法是绝对正确的,但是所有这三个方法在可读性上都非常接近,以至于在这种情况下,学步法只是学问法。


3

我一直了解到乘法更有效。


“高效”是错误的词。的确,大多数处理器的乘法速度快于其除法速度。但是,使用现代的流水线架构,您的程序可能看不到任何区别。由于很多人都这么说,你真是方式最好只在做什么读最好的人。
TED

3

乘法通常更快-当然绝对不会慢。但是,如果速度不是很关键,则以最清晰的方式写入。


2

浮点除法(通常)特别慢,因此尽管浮点乘法也相对较慢,但它可能比浮点除法更快。

但是我更倾向于回答“这并不重要”,除非分析表明除法相对于乘法而言有点瓶颈。不过,我猜测乘法与除法的选择不会对您的应用程序产生很大的性能影响。


2

当您使用汇编语言或C语言进行编程时,这将成为一个问题。我认为,对于大多数现代语言而言,诸如此类的优化都在为我完成。


2

警惕“猜测乘法通常更好,因此在编写代码时我会坚持这样做,”

在这个特定问题的上下文中,更好的意思是“更快”。这不是很有用。

考虑速度可能是一个严重的错误。在特定的代数形式的计算中存在深刻的误差含义。

请参阅带有误差分析的浮点算法。请参见浮点算法和误差分析中的基本问题

尽管某些浮点值是准确的,但大多数浮点值是一个近似值。它们是一些理想值加上一些误差。每个操作都适用于理想值和误差值。

最大的问题来自试图操纵两个几乎相等的数字。最右边的位(错误位)开始支配结果。

>>> for i in range(7):
...     a=1/(10.0**i)
...     b=(1/10.0)**i
...     print i, a, b, a-b
... 
0 1.0 1.0 0.0
1 0.1 0.1 0.0
2 0.01 0.01 -1.73472347598e-18
3 0.001 0.001 -2.16840434497e-19
4 0.0001 0.0001 -1.35525271561e-20
5 1e-05 1e-05 -1.69406589451e-21
6 1e-06 1e-06 -4.23516473627e-22

在此示例中,您可以看到,当值变小时,几乎相等的数字之间的差会产生非零结果,其中正确答案为零。


1

我读过某个地方,乘法在C / C ++中更有效。关于解释语言一无所知-由于其他所有开销,差异可能微不足道。

除非它成为一个问题,否则要坚持什么更具可维护性/可读性-当人们告诉我这是事实时,我讨厌它。


1

我一般建议使用乘法,因为您不必花费时间来确保除数不为0。如果除数是常数,则这并不适用。


1

在Samsung GT-S5830上分析的Java android

public void Mutiplication()
{
    float a = 1.0f;

    for(int i=0; i<1000000; i++)
    {
        a *= 0.5f;
    }
}
public void Division()
{
    float a = 1.0f;

    for(int i=0; i<1000000; i++)
    {
        a /= 2.0f;
    }
}

结果呢?

Multiplications():   time/call: 1524.375 ms
Division():          time/call: 1220.003 ms

除法比乘法快约20%(!)


1
现实地说,您应该测试a = i*0.5而不是a *= 0.5。这就是大多数程序员将使用这些操作的方式。
Blazemonger,2011年

1

与第24个帖子(乘法更快)和第30个帖子一样,但有时它们都一样容易理解:

1*1e-6F;

1/1e6F;

〜我发现它们都一样容易阅读,并且不得不重复数十亿次。因此了解乘法通常会更快是很有用的。


1

有所不同,但这取决于编译器。最初在vs2003(c ++)上,我对双精度类型(64位浮点数)没有明显的不同。但是,在vs2010上再次运行测试时,我发现了巨大的差异,乘法运算的速度提高了4倍。追根溯源,似乎vs2003和vs2010会生成不同的fpu代码。

在Pentium 4、2.8 GHz和2003版上:

  • 乘法:8.09
  • 部门:7.97

在Xeon W3530和vs2003上:

  • 乘法:4.68
  • 师:4.64

在Xeon W3530和vs2010上:

  • 乘法:5.33
  • 师:21.05

似乎在vs2003上,循环中的除法(因此多次使用了除数)被转换为与逆的乘法。在vs2010上,不再应用此优化(我想是因为两种方法之间的结果略有不同)。还请注意,分子为0.0时,CPU会更快地执行除法运算。我不知道芯片中硬连接的精确算法,但是也许它取决于数字。

编辑18-03-2013:vs2010的观察


我想知道是否有任何原因编译器无法替换n/10.0形式的表达式(n * c1 + n * c2)?我希望在大多数处理器上,一个除法运算将花费比两个乘法和一个除法运算更长的时间,而且我相信,在任何情况下,使用所示公式进行除以任何常数都可以得出正确舍入的结果。
supercat 2014年

1

这是一个愚蠢的有趣答案:

X / 2.0等同于X * 0.5

假设您是在2008年10月22日编写此方法的。

double half(double x) => x / 2.0;

十年后的今天,您了解到可以优化这段代码。在整个应用程序中,数百种公式中都引用了该方法。因此,您对其进行了更改,并获得了5%的显着性能提升。

double half(double x) => x * 0.5;

更改代码是正确的决定吗?在数学中,这两个表达式的确相等。在计算机科学中,并不总是如此。有关更多详细信息,请阅读最小化准确性问题的影响。如果您的计算值在某些时候与其他值进行了比较,则将更改边缘情况的结果。例如:

double quantize(double x)
{
    if (half(x) > threshold))
        return 1;
    else
        return -1;
}

底线是; 一旦您解决了这两个问题中的任何一个,那就坚持下去吧!


1
下注?解释您的想法的评论怎么样?这个答案绝对是100%相关的。
l33t

在计算机科学中,除非浮点值变得非正规化或溢出,否则将浮点值乘以2的乘方/除是无损的。
即将

由于浮点在除法时并不是无损的,因此您的陈述是否正确并不重要。虽然我会很惊讶。
l33t

1
“只有在使用发出不推荐使用的x87代码的古老编译器进行构建时,“浮点在分割时并不是无损的”。在现代硬件上,只有float / double变量是无损的,无论是32位还是64位的IEEE 754:en.wikipedia.org/wiki/IEEE_754由于IEEE 754的工作方式,当您除以2或乘以0.5时,您会减少指数乘以1,其余位(符号+尾数)不变。而且20.5数字和数字都可以在IEEE 754中准确表示,而不会损失任何精度(与例如0.4或一样0.1,它们不能这样)。
即将

0

好吧,如果我们假设添加/子轨道操作的成本为1,则乘以成本5,除以成本为20。


您从哪里得到这些数字?经验?直觉?互联网上的文章?对于不同的数据类型,它们将如何变化?
kroiz 2014年

0

在进行了如此漫长而有趣的讨论之后,我将对此进行分析:这个问题没有最终答案。正如某些人指出的那样,这取决于硬件(cf piotrkgast128)和编译器(cf @Javier的测试)。如果速度不是很关键,则如果您的应用程序不需要实时处理大量数据,则可以使用除法来选择清晰性,而如果处理速度或处理器负载是一个问题,则乘法可能是最安全的。最后,除非您确切知道要在哪个平台上部署应用程序,否则基准测试毫无意义。为了使代码清晰,只需添加一个注释即可!


-3

从技术上讲,没有除法之类的东西,只有逆元素相乘。例如,您永远不会除以2,实际上是乘以0.5。

“司” -让我们自欺欺人,它的存在是有第二个-是总是很难乘法因为“鸿沟” x通过y首先需要计算的值y^{-1},使得y*y^{-1} = 1然后做乘法x*y^{-1}。如果您已经知道,y^{-1}则不进行计算y必须是一种优化。


3
这完全忽略了芯片中存在的两个命令的实际情况。
NPSF3000

@ NPSF3000-我不关注。在两个运算都存在的假设下,它简单地断言除法运算隐式地涉及乘法逆和乘法的计算,这总是比仅进行一次乘法困难。芯片是实现细节。
satnhak 2012年

@BTyler。如果两个命令都存在于芯片中,并且两个命令所花费的周期数相同(如人们所预期的),那么指令的相对复杂程度与性能POV完全无关。
NPSF3000

@ NPSF3000-但它们的周期数并不相同,因为乘法速度更快。
satnhak 2012年
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.