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

134

这里有一个有趣的问题:

假设我们需要执行一个简单操作,其中需要使用变量值的一半。通常有两种方法实现:

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

假设我们使用语言提供的标准运算符,哪个运算符性能更好?
我猜乘法通常更好,所以我在编码时尽量使用它,但我想确认一下。
虽然我个人对Python 2.4-2.5的答案感兴趣,但也可以为其他语言发布答案!如果您愿意,也可以发布其他更高级的方法(如使用位移运算符)。

5
你运行了基准测试吗?这只需要大约十几行的代码。从运行基准测试中学到了什么?(提示:这样做比在这里发布问题更快。) - S.Lott
4
好问题,引发了一些相当有趣的答案/讨论。谢谢 :) - stealthcopter
37
即使他通过基准测试学到了答案,这仍然是一个有用的问题,并且产生了一些有趣和有用的答案。同时我希望人们能够抓住重点,避免在回答和评论中提供与是否值得进行优化等无关的建议。为什么不假设问问题的人就是按照所写的问题来询问,而不是假设他或她“真正”想获得关于更大范围重写的建议。 - Kevin Whitefoot
1
除法比乘法慢得多。但是一些聪明的编译器/虚拟机会将除法转换为乘法,因此您的测试将具有相同的结果(两个测试都测试乘法)。 - Ivan Kuckir
6
有点离题,但我想说我非常同意@KevinWhitefoot的观点。与其听道德说教者的话,不如直接给出技术问题的明确技术答案,这真是令人沮丧。感谢Kevin的评论! - Jean-François
显示剩余2条评论
25个回答

86

Python:

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%

Lua:

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中,乘法比除法更快,但随着使用更先进的虚拟机或JITs靠近CPU,优势会消失。未来的Python虚拟机可能会使其变得不相关。


感谢您提供有关使用时间命令进行基准测试的提示! - edmundito
2
你的结论是错误的。随着JIT/VM的改进,它变得更加相关。与VM的较低开销相比,除法会变慢。请记住,编译器通常不能优化浮点运算,以保证精度。 - rasmus
9
随着JIT编译器的改进,即使您要求除法操作,它也更有可能使用CPU乘法指令。 - Ben Voigt

74

始终使用最清晰的方法,其他方法都是试图比编译器更聪明。如果编译器有一点智能,它会尽最大努力优化结果,但没有什么能阻止下一个人因为你的可怜的位操作解决方案而憎恨你 (我顺便说一下,位操作很有趣,但有趣 ≠ 可读)

过早地进行优化是万恶之源,永远记住优化的三个规则!

  1. 不要做优化。
  2. 如果您是专家,请参见规则#1
  3. 如果您是专家并且可以证明需要进行优化,则应使用以下流程:

    • 编写未经优化的代码
    • 确定多快才算“足够快”——请注意哪个用户需求/故事需要该度量标准。
    • 编写速度测试
    • 测试现有代码——如果足够快,那就完成了。
    • 重新编写进行优化的代码
    • 测试经优化的代码。如果不符合度量标准,请将其丢弃并保留原始代码。
    • 如果通过测试,请将原始代码作为注释保留。

另外,像在不需要的情况下删除内部循环或选择链接列表进行插入排序而不是数组等操作并非优化,而只是编程。


8
这不是 Knuth 的完整引用;请参考 http://en.wikipedia.org/wiki/Optimization_(computer_science)#When_to_optimize。 - Jason S
不,关于这个主题大约有40个不同的引语来自于许多不同的来源。我会把其中几个拼凑在一起。 - Bill K
你最后一句话让人不清楚何时应用规则#1和#2,让我们回到了起点:我们需要决定哪些优化是值得的,哪些不值得。假装答案很明显并不是一个答案。 - Matt
2
这对你来说真的很困惑吗?除非你实际上不符合客户规格并且非常熟悉整个系统,包括CPU的语言和缓存特性,否则始终遵循规则1和2。在那时,只需按照步骤3进行操作,不要仅仅认为“嘿,如果我将此变量本地缓存而不是调用getter,事情可能会更快。”首先证明它不够快,然后逐个测试每个优化,并且放弃无用的优化。一路上要有大量记录。 - Bill K

49

我认为这变得太过吹毛求疵,你最好做任何可以让代码更易读的事情。除非你执行操作数以千计甚至百万计,否则我怀疑任何人都不会注意到差异。

如果你真的必须做出选择,基准测试是唯一的方法。找出哪些函数给你带来了问题,然后找出在函数中哪些部分出现了问题,并修复这些部分。然而,我仍然怀疑单个数学运算(即使重复多次)也不会成为任何瓶颈的原因。


1
当我从事雷达处理器制造时,单个操作确实会产生影响。但我们手动优化机器代码以实现实时性能。对于其他所有事情,我投票支持简单明了。 - S.Lott
我猜对于某些事情,你可能关心单个操作。但我预计在99%的应用程序中,这并不重要。 - Thomas Owens
27
特别是因为该OP正在寻找Python的答案,我怀疑任何需要如此高效的东西都不会用Python编写。 - Ed S.
4
在三角形相交例程中,划分可能是最昂贵的操作,这是大多数光线追踪器的基础。如果你存储倒数并进行乘法运算而不是除法,你将会体验到数倍的加速。 - solinent
@JasonS,4:1听起来对我来说是一个相当显著的改进。 “什么?你把游戏从15帧每秒提升到60帧每秒?那微不足道。你被解雇了!!!” - user1593842
显示剩余2条评论

42

乘法更快,除法更准确。如果你的数字不是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语言中才有可能成为问题,而且仅仅当操作在瓶颈处的循环中时才会成为问题。


如果你正在除以整数,那么除法是准确的。 - plinth
7
分母大于分子的浮点数除法会在低位比特中引入无意义的值,一般来说,除法会降低精度。 - S.Lott
8
@S.Lott:不,那不是真的。符合IEEE-754标准的所有浮点数实现都必须根据当前的舍入模式完美地四舍五入每个操作的结果(即最接近的浮点数)。通过乘以倒数始终会引入更多的误差,至少因为需要进行一次额外的舍入。 Translated: @S.Lott: 不,这不是正确的。所有符合IEEE-754标准的浮点数实现,必须根据当前的舍入模式完美地将每个操作的结果舍入到最接近的浮点数。通过乘以倒数总是会引入更多的误差,至少因为需要进行一次额外的舍入。 - Electro
1
我知道这个回答已经超过8年了,但它是误导性的;你可以在不显著损失精度的情况下执行除法:y = x * (1.0/3.0);,并且编译器通常会在编译时计算1/3。是的,1/3在IEEE-754中不能完美地表示,但当你执行浮点运算时,无论是乘法还是除法,你都会失去精度,因为低位的位会被舍入。如果你知道你的计算对舍入误差如此敏感,你也应该知道如何最好地处理这个问题。 - Jason S
3
@JasonS 我刚刚让一个程序在夜间运行,从1.0开始,每次增加1 ULP;我将乘以(1.0/3.0)的结果与除以3.0的结果进行了比较。我一直比较到1.0000036666774155,在这个范围内有7.3%的结果是不同的。我认为它们只是相差1位,但由于IEEE算术保证四舍五入到最接近的正确结果,所以我坚持认为除法更准确。这种差异是否显著取决于你。 - Mark Ransom
@JasonS 举个失败的例子,试试输入1.0009765625。 - Mark Ransom

26

如果你想优化你的代码并且仍然保持清晰易懂,可以尝试这个方法:

y = x * (1.0 / 2.0);

编译器应该能够在编译时进行除法运算,因此您将在运行时获得乘法运算。我期望精度与 y = x / 2.0 的情况相同。

这在需要使用浮点数计算时需要进行浮点数模拟的嵌入式处理器中可能非常重要。


13
请您自便(和那位点踩的人)——在嵌入式领域,这是标准做法,该领域的软件工程师认为这很清晰明了。 - Jason S
4
+1 因为你是唯一一个意识到编译器不能随意优化浮点运算的人。它们甚至不能改变乘法运算中操作数的顺序以保证精度(除非使用松弛模式)。 - rasmus
1
哎呀,至少有6个程序员认为初等数学不清楚。据我所知,IEEE 754乘法是可交换的(但非结合)。 - maaartinus
13
也许你没有理解重点。这与代数正确性无关。在理想的世界里,你应该只需要将其除以二:y = x / 2.0;,但在现实世界中,你可能需要劝诱编译器执行一个更便宜的乘法。也许y = x * (1.0 / 2.0);为什么更好不是很清楚,用y = x * 0.5;来表述可能更清晰。但是如果将2.0改为7.0,我更愿意看到y = x * (1.0 / 7.0);而不是y = x * 0.142857142857; - Jason S
3
使用你的方法更易读(和精确),这真的很清楚。 - Juan Martinez
@rasmus:具有讽刺意味的是,这个特定的例子是编译器可以在不违反严格FP语义的情况下进行此优化的一种情况,像GCC和clang这样的真正的编译器确实会这样做。2.0是2的幂,因此它的倒数0.5可以精确地表示为浮点数或双精度浮点数。请参见为什么编译器不将“n / 2.0”强制转换为“n * 0.5”,如果速度更快?作为一个例子(如果启用优化,则会执行此操作,如答案所示)。对于不能精确表示其倒数的除数,您必须手动执行此操作或使用-ffast-math以获得速度。 - Peter Cordes

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)

然而,当我将那些int改成double(这也是Python可能会做的),我得到了这个结果:

division:

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的性能取决于编程语言和平台的实现。最终的差异是微不足道的,你几乎永远不需要担心它,除非从可读性的角度来考虑。

另外一件事,当我去掉volatile关键字时,你可能无法猜出我的程序main()返回的汇编代码长什么样子(排除了程序设置):

## 5/2

## 5*0.5
## done

movl    $5, %eax
leave
ret

它在一条指令中完成了除法、乘法和加法!如果优化器足够尊重,你就不必担心这个问题。

对于过长的回答表示抱歉。


1
这不是“单个指令”。它只是被常量折叠了。 - kvanbere
5
当然,它是一条指令。数一下:movl $5, %eax优化的名称并不重要,甚至与此无关。你只是想在一个四年前的答案上表现得居高临下。 - Carson Myers
2
优化的本质仍然很重要,因为它是上下文敏感的:只适用于添加/乘/除等编译时常量的情况,在这种情况下,编译器可以提前执行所有数学运算并将最终答案移动到运行时寄存器中。在一般情况下(运行时除数),除法比乘法慢得多,但我想如果您否则需要多次除以相同的分母,则仅通过倒数相乘才有所帮助。您可能已经知道所有这些,但新手程序员可能需要详细说明,所以......以防万一。 - Mike S
为什么只有-O1?另外,更好的查看汇编代码的方法是编写一个带参数并返回值的函数。如果你只是查看汇编代码,就不需要或者不想要一个main函数。(特别是在GCC中,你需要避免使用main函数,因为它对于main有一个隐式的__attribute__((cold))属性。)但是,将整数乘以0.5是完全错误的,特别是在旧版x87中,从浮点数到整数的截断非常不方便,必须改变舍入模式。 - Peter Cordes

11
首先,除非您正在使用C或装配语言编程,否则您可能在更高级别的语言中编写代码,其中内存延迟和普通调用开销将绝对比乘法和除法之间的差异要大到可以忽略不计的程度。因此,在这种情况下,只需选择更易读的即可。
如果您从非常高的层面谈起,您将发现对于您可能使用它的任何内容,它的速度不会慢多少。你会发现其他答案中,人们需要进行一百万次乘法/除法才能测量两者之间的一些亚毫秒差异。
如果您仍然好奇,从低级优化的角度来看:
除法往往比乘法的指令流水线要长得多。这意味着需要更长时间才能获得结果,但是如果您可以让处理器忙于非相关任务,那么与乘法相比,它并不会花费更多时间。
在硬件上这种管道差距是完全依赖于硬件的。我最后使用过的硬件是FPU乘法需要9个时钟周期,而FPU除法需要50个时钟周期。听起来很多,但是如果出现内存缺失,您会失去1000个时钟周期,因此这可以使事情变得明朗。
一个比喻是当您观看电视节目时,将馅饼放入微波炉中。将馅饼放入微波炉中以及从微波炉中取出所花费的时间是总共让您远离电视节目的时间。在其余时间中,您仍然观看电视节目。因此,如果馅饼需要10分钟烹制而不是1分钟,它实际上并没有占用您更多的电视观看时间。
实际上,如果您要关心乘法和除法之间的差异,您需要了解流水线、高速缓存、分支延迟、乱序预测和管道依赖性。如果这听起来与您想问的不同,则正确的答案是忽略两者之间的差异。
许多(许多)年前,避免使用除法并始终使用乘法是绝对至关重要的,但那时内存命中率不那么重要,而且除法情况更糟。如今我更注重可读性,但如果没有可读性差异,我认为选择乘法是一个好习惯。

8

写出更清晰地表达您意图的方式。

在程序能够运行后,找出哪些部分速度较慢,并让它们变得更快。

不要反过来做。


6

尽管你需要的任何操作。首先考虑你的读者,不要担心性能问题,直到你确定存在性能问题。

让编译器为你处理性能问题。


6
实际上,通常乘法比除法更快有一个很好的理由。硬件中的浮点除法是通过移位和条件减算法(使用二进制数的“长除法”)或者像戈尔德斯密特算法这样的迭代来完成的(现在更可能是后者)。移位和减法需要每个精度位至少一个周期(迭代几乎不可能并行化,就像乘法的移位和加法一样),而迭代算法在每次迭代中至少进行一次乘法。在任何情况下,除法很可能需要更多的周期。当然,这并没有考虑编译器、数据移动或精度中的怪癖。总的来说,如果您正在编写程序中时间敏感部分的内部循环,写成0.5 * x1.0/2.0 * x 而不是 x / 2.0 是合理的。 "编写清晰代码" 的严谨性绝对正确,但这三种方法在可读性上非常接近,所以在这种情况下,严谨性只是纠结。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接