这是一个愚蠢的有趣问题:
假设我们必须执行一个简单的操作,其中我们需要变量值的一半。有通常这样做的方法有两种:
y = x / 2.0;
// or...
y = x * 0.5;
假设我们使用的是语言随附的标准运算符,那么哪一种具有更好的性能?
我猜想乘法通常更好,所以我在编码时会尽量坚持下去,但我想确认一下。
尽管我个人对Python 2.4-2.5 的答案感兴趣,但是也可以发布其他语言的答案!而且,如果您愿意,也可以发布其他更奇特的方式(例如使用按位移位运算符)。
这是一个愚蠢的有趣问题:
假设我们必须执行一个简单的操作,其中我们需要变量值的一半。有通常这样做的方法有两种:
y = x / 2.0;
// or...
y = x * 0.5;
假设我们使用的是语言随附的标准运算符,那么哪一种具有更好的性能?
我猜想乘法通常更好,所以我在编码时会尽量坚持下去,但我想确认一下。
尽管我个人对Python 2.4-2.5 的答案感兴趣,但是也可以发布其他语言的答案!而且,如果您愿意,也可以发布其他更奇特的方式(例如使用按位移位运算符)。
Answers:
蟒蛇:
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很可能会使其变得无关紧要
始终使用最清晰的东西。您所做的任何其他操作都试图使编译器的性能超越智能。如果编译器是完全智能的,它将尽最大努力优化结果,但是没有什么可以使下一个家伙不讨厌您讨厌的笨拙的位移解决方案(顺便说一句,我喜欢位操作,很有趣。但是很有趣!=可读) )
过早的优化是万恶之源。永远记住优化的三个规则!
如果您是专家并且可以证明需要,那么请使用以下过程:
同样,执行诸如在不需要时删除内部循环或为插入排序选择数组上的链表之类的操作也不是优化,而只是编程。
我认为这变得太挑剔了,您最好做任何使代码更具可读性的事情。除非您执行数千次(甚至数百万次)操作,否则我怀疑有人会注意到这种差异。
如果您真的必须做出选择,则基准测试是唯一的选择。查找哪些功能给您带来了问题,然后找出功能中出现问题的位置,然后修复这些部分。但是,我仍然怀疑单个数学运算(甚至重复多次,多次)是否会引起任何瓶颈。
乘法更快,除法更准确。如果您的数字不是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语言中才有关系,甚至在操作陷入瓶颈时也是如此。
y = x * (1.0/3.0);
并且编译器通常会在编译时计算1/3。是的,1/3是不完全可表示在IEEE-754,但是当你执行浮点运算你失去精度反正,不管你是在做乘法或除法,因为低位是圆形的。如果您知道您的计算对舍入误差非常敏感,那么您还应该知道如何最好地处理该问题。
(1.0/3.0)
除以的结果进行了比较3.0
。我的分数高达1.0000036666774155,在那个空间中7.3%的结果有所不同。我认为它们之间只有1位的差异,但是由于保证IEEE算术可以舍入到最接近的正确结果,因此我坚持说除法更准确。差异是否显着取决于您。
如果您想优化代码,但仍然很清楚,请尝试以下操作:
y = x * (1.0 / 2.0);
编译器应该能够在编译时进行除法,因此您可以在运行时进行乘法。我希望精度与y = x / 2.0
情况相同。
LOT可能在嵌入式处理器中需要浮点仿真来计算浮点算术,这点很重要。
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;
。
只是要为“其他语言”选项添加一些内容。
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
s 更改为double
s(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
它在一条指令中完成了除法,乘法和加法运算!显然,如果优化程序是任何受人尊敬的,您都不必为此担心。
抱歉,答案太长了。
movl $5, %eax
优化的名称并不重要,甚至不相关。您只是想屈服于四年的答案。
首先,除非您使用C或ASSEMBLY进行工作,否则您可能使用的是高级语言,其中内存停滞和常规调用开销将使相乘和相除之间的差异完全相形见to。因此,只要选择在这种情况下更好的方法即可。
如果您是从很高的级别进行交谈,那么您可能会用它来测量任何事情的速度都不会明显变慢。您还会在其他答案中看到,人们需要做一百万次乘/除运算,才能测量两者之间的亚毫秒级差异。
如果您仍然好奇,请从低级优化的角度来看:
除法往往具有比乘积更长的流水线。这意味着获得结果要花费更长的时间,但是如果您可以让处理器忙于执行非相关任务,那么最终花费的成本不会超过乘法。
流水线差异有多长完全取决于硬件。我使用的最后一个硬件是FPU乘法的9个周期和FPU除法的50个周期。听起来很多,但随后您会因内存不足而丢失1000个周期,因此可以将其视为现实。
打个比方,就是在看电视节目时把馅饼放在微波炉里。将您带离电视节目的总时间是将其放入微波炉中并从微波炉中取出要花费多长时间。剩下的时间,您仍然看电视节目。因此,如果该饼花了10分钟而不是1分钟来煮,那么它实际上并没有用光电视观看时间。
在实践中,如果您要关注乘法和除法之间的差异,则需要了解管道,缓存,分支停顿,无序预测和管道依赖性。如果这听起来不像您打算解决的问题,那么正确的答案就是忽略两者之间的区别。
许多(许多)年前,绝对重要的是要避免使用分隔符并始终使用乘法,但是那时候记忆命中的意义不大,而分隔符则更为糟糕。这些天来,我对可读性的评价更高,但是如果没有可读性差异,我认为选择乘数是一个好习惯。
写出任何更清楚表明您意图的东西。
程序运行后,找出缓慢的地方,并加快速度。
不要反过来做。
如果您使用整数或非浮点类型,请不要忘记您的移位运算符:<< >>
int y = 10;
y = y >> 1;
Console.WriteLine("value halved: " + y);
y = y << 1;
Console.WriteLine("now value doubled: " + y);
实际上,有充分的理由认为,作为一般经验法则,乘法将比除法更快。硬件中的浮点除法是通过移位和条件减法算法(带有二进制数的“长除法”)完成的,或者(最近很可能是)通过类似于Goldschmidt算法的迭代完成的。移位和减法每位精度至少需要一个周期(几乎不可能像乘法一样进行迭代并行化),并且迭代算法至少要进行一次乘法每次迭代。无论哪种情况,该部门极有可能需要更多的周期。当然,这不考虑编译器中的怪癖,数据移动或精度。但是,总的来说,如果要在程序的时间敏感部分中编码一个内部循环,那么编写0.5 * x
或1.0/2.0 * x
而不是x / 2.0
这样做是合理的。“编写最清晰的代码”的学步法是绝对正确的,但是所有这三个方法在可读性上都非常接近,以至于在这种情况下,学步法只是学问法。
警惕“猜测乘法通常更好,因此在编写代码时我会坚持这样做,”
在这个特定问题的上下文中,更好的意思是“更快”。这不是很有用。
考虑速度可能是一个严重的错误。在特定的代数形式的计算中存在深刻的误差含义。
请参阅带有误差分析的浮点算法。请参见浮点算法和误差分析中的基本问题。
尽管某些浮点值是准确的,但大多数浮点值是一个近似值。它们是一些理想值加上一些误差。每个操作都适用于理想值和误差值。
最大的问题来自试图操纵两个几乎相等的数字。最右边的位(错误位)开始支配结果。
>>> 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
在此示例中,您可以看到,当值变小时,几乎相等的数字之间的差会产生非零结果,其中正确答案为零。
我读过某个地方,乘法在C / C ++中更有效。关于解释语言一无所知-由于其他所有开销,差异可能微不足道。
除非它成为一个问题,否则要坚持什么更具可维护性/可读性-当人们告诉我这是事实时,我讨厌它。
在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%(!)
a = i*0.5
而不是a *= 0.5
。这就是大多数程序员将使用这些操作的方式。
有所不同,但这取决于编译器。最初在vs2003(c ++)上,我对双精度类型(64位浮点数)没有明显的不同。但是,在vs2010上再次运行测试时,我发现了巨大的差异,乘法运算的速度提高了4倍。追根溯源,似乎vs2003和vs2010会生成不同的fpu代码。
在Pentium 4、2.8 GHz和2003版上:
在Xeon W3530和vs2003上:
在Xeon W3530和vs2010上:
似乎在vs2003上,循环中的除法(因此多次使用了除数)被转换为与逆的乘法。在vs2010上,不再应用此优化(我想是因为两种方法之间的结果略有不同)。还请注意,分子为0.0时,CPU会更快地执行除法运算。我不知道芯片中硬连接的精确算法,但是也许它取决于数字。
编辑18-03-2013:vs2010的观察
n/10.0
形式的表达式(n * c1 + n * c2)
?我希望在大多数处理器上,一个除法运算将花费比两个乘法和一个除法运算更长的时间,而且我相信,在任何情况下,使用所示公式进行除以任何常数都可以得出正确舍入的结果。
这是一个愚蠢的有趣答案:
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;
}
底线是; 一旦您解决了这两个问题中的任何一个,那就坚持下去吧!
2
,0.5
数字和数字都可以在IEEE 754中准确表示,而不会损失任何精度(与例如0.4
或一样0.1
,它们不能这样)。
从技术上讲,没有除法之类的东西,只有逆元素相乘。例如,您永远不会除以2,实际上是乘以0.5。
“司” -让我们自欺欺人,它的存在是有第二个-是总是很难乘法因为“鸿沟” x
通过y
首先需要计算的值y^{-1}
,使得y*y^{-1} = 1
然后做乘法x*y^{-1}
。如果您已经知道,y^{-1}
则不进行计算y
必须是一种优化。