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

时间:2008-10-22 16:01:23

标签: performance programming-languages

这是一个非常有趣的问题:

假设我们需要执行一个简单的操作,我们需要一半的变量值。 通常有两种方法可以做到这一点:

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

假设我们正在使用该语言提供的标准运算符,哪一个具有更好的性能?

我猜测乘法通常更好,所以我在编码时会尽力坚持,但我想证实这一点。

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

25 个答案:

答案 0 :(得分:73)

的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中,乘法比分割更快,但随着使用更高级的VM或JIT越来越接近CPU,优势就消失了。未来的Python VM很可能会使它变得无关紧要

答案 1 :(得分:62)

始终使用最清楚的东西。你做的其他事情就是试图超越编译器。如果编译器完全是智能的,它会尽力优化结果,但没有什么可以让下一个人不讨厌你的糟糕的位移解决方案(顺便说一句,我喜欢点操作,这很有趣。但很有趣!=可读)

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

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

    • 编码未经优化
    • 确定“足够快”的速度 - 请注意哪个用户要求/故事需要该指标。
    • 编写速度测试
    • 测试现有代码 - 如果速度足够快,就完成了。
    • 重新编码优化
    • 测试优化代码。如果它不符合指标,请将其扔掉并保留原文。
    • 如果符合测试,请将原始代码保留为注释
  4. 此外,在不需要内部循环时执行删除内部循环或在数组上为插入排序选择链接列表不是优化,只是编程。

答案 2 :(得分:47)

我认为这样变得非常挑剔,以至于你最好不要做任何使代码更具可读性的东西。除非你执行数千次,甚至数百万次的操作,否则我怀疑任何人都会注意到这种差异。

如果你真的需要做出选择,那么基准测试是唯一的出路。找出哪些函数给你带来问题,然后找出问题出现在函数中的哪个位置,并修复这些部分。但是,我仍然怀疑一个单一的数学运算(即使重复了很多次)也会导致任何瓶颈。

答案 3 :(得分:36)

乘法更快,除法更准确。如果你的数字不是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语言中才有意义,即使这样,只有在操作处于瓶颈的循环中时才会出现问题。

答案 4 :(得分:24)

如果您想优化代码但仍然清晰,请尝试以下方法:

y = x * (1.0 / 2.0);

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

在这可能很重要的情况下,LOT在嵌入式处理器中,其中需要浮点仿真来计算浮点运算。

答案 5 :(得分:20)

只是为“其他语言”选项添加内容。
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 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()返回a + b。当我拿走volatile关键字时,你永远不会猜到程序集的样子(不包括程序设置):

## 5/2

## 5*0.5
## done

movl    $5, %eax
leave
ret

它在单个指令中完成了除法,乘法和加法!显然,如果优化器有任何可敬的话,你不必担心这个。

对于过长的回答感到抱歉。

答案 6 :(得分:9)

首先,除非您在C或ASSEMBLY工作,否则您可能处于更高级别的语言,其中内存停滞和一般呼叫开销绝对会使乘法和除法之间的差异相形见绌。所以,在这种情况下,只需选择更好的读数。

如果您从非常高的级别谈话,那么对于您可能会使用它的任何内容来说,它的速度都会慢得多。你会在其他答案中看到,人们需要做一百万乘法/除法才能测量两者之间的亚毫秒差异。

如果您仍处于好奇状态,从低级优化的角度来看:

Divide往往具有比乘法更长的管道。这意味着获得结果需要更长的时间,但如果您可以让处理器忙于处理非依赖性任务,那么它最终不会花费您的成本。

管道差异的长度完全取决于硬件。我使用的最后一个硬件类似于FPU乘法的9个周期和FPU除法的50个周期。听起来很多,但是你会因为记忆失误而失去1000个周期,所以这可以把事情放在眼里。

类比是在观看电视节目时将馅饼放入微波炉中。你离开电视节目的总时间是将它放入微波炉并将其从微波炉中取出多长时间。剩下的时间你还在观看电视节目。因此,如果馅饼需要10分钟来烹饪而不是1分钟,那么它实际上不会消耗你的电视观看时间。

在实践中,如果您要达到关注Multiply和Divide之间差异的程度,您需要了解管道,缓存,分支停顿,无序预测和管道依赖性。如果这听起来不像你想要回答这个问题,那么正确的答案是忽略两者之间的差异。

许多(许多)年前,避免分歧绝对是至关重要的,并且总是使用倍数,但当时记忆命中的相关性较低,而且分歧更加严重。这些天我对可读性的评价更高,但如果没有可读性差异,我认为选择倍增是一个好习惯。

答案 7 :(得分:7)

写下更明确说明你的意图的任何一个。

在你的程序运作之后,弄清楚什么是慢的,并加快速度。

不要反过来做。

答案 8 :(得分:6)

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

让编译器为你做好表现。

答案 9 :(得分:4)

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

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

答案 10 :(得分:4)

实际上有一个很好的理由,作为一般的经验法则,乘法比分裂更快。硬件中的浮点除法使用移位和条件减法算法(带有二进制数的“长除法”)或者 - 更有可能 - 使用Goldschmidt's算法等迭代进行。移位和减法每位精度至少需要一个周期(迭代几乎不可能与乘法的移位和加法并行化),并且迭代算法每次迭代至少进行一次乘法。在任何一种情况下,该部门很可能需要更多周期。当然,这并不能解释编译器,数据移动或精度方面的怪癖。但是,总的来说,如果您在程序的时间敏感部分编写内部循环,则编写0.5 * x1.0/2.0 * x而不是x / 2.0是合理的。 “代码什么是最清楚的”的迂腐是完全正确的,但是这三者在可读性上都非常接近,以至于在这种情况下,迂腐只是迂腐。

答案 11 :(得分:3)

乘法通常更快 - 当然永远不会慢。 但是,如果它不是速度关键,请写出最清楚的。

答案 12 :(得分:2)

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

但我更倾向于回答“它并不重要”,除非剖析表明除法与乘法相比有点瓶颈。不过,我猜测乘法与除法的选择不会对你的应用产生很大的性能影响。

答案 13 :(得分:2)

当你在汇编或C语言编程时,这就变成了一个问题。我认为,对于大多数现代语言来说,正在为我做这样的优化。

答案 14 :(得分:2)

警惕“猜测乘法通常更好,所以当我编码时我会坚持这样做”,

在这个具体问题的背景下,这里更好的意思是“更快”。这不是很有用。

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

Floating Point arithmetic with error analysis。见Basic Issues in Floating Point Arithmetic and Error Analysis

虽然一些浮点值是精确的,但大多数浮点值都是近似值;他们是一些理想的价值加上一些错误。每个操作都适用于理想值和误差值。

最大的问题来自于试图操纵两个几乎相等的数字。最右边的位(错误位)决定了结果。

>>> 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

在这个例子中,您可以看到随着值变小,几乎相等的数字之间的差异会在正确答案为零的情况下产生非零结果。

答案 15 :(得分:2)

我一直都知道乘法更有效率。

答案 16 :(得分:1)

我会建议乘法一般,因为你不必花费周期来确保你的除数不是0.当然,如果你的除数是常数,那么这不适用。

答案 17 :(得分:1)

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

除非它成为一个问题,坚持使用更易于维护/可读的东西 - 我讨厌人们告诉我这一点,但它是如此真实。

答案 18 :(得分:1)

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

1*1e-6F;

1/1e6F;

〜我发现它们都很容易阅读,并且必须重复数十亿次。因此,知道乘法通常更快是有用的。

答案 19 :(得分:1)

Java android,在Samsung GT-S5830上描述

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%

答案 20 :(得分:1)

存在差异,但它依赖于编译器。首先在vs2003(c ++)上,我对双类型(64位浮点)没有显着差异。然而,在vs2010上再次运行测试,我发现了一个巨大的差异,乘法更快到4倍。跟踪此情况,似乎vs2003和vs2010生成不同的fpu代码。

Pentium 4,2.8 GHz,vs2003:

  • 乘法: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的观察

答案 21 :(得分:1)

这是一个非常有趣的答案:

x / 2.0 等同于 x * 0.5

我们假设你在2008年10月22日写了这个方法。

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

现在,10年后,您了解到可以优化这段代码。在整个应用程序中,该方法在数百个公式中被引用。所以你改变了它,并且经历了5%的显着性能提升。

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

更改代码是正确的决定吗?在数学中,这两个表达式确实是等价的。在计算机科学中,这并不总是成立。有关详细信息,请阅读Minimizing the effect of accuracy problems。如果您的计算值 - 在某个时刻 - 与其他值进行比较,您将更改边缘情况的结果。 E.g:

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

底线是;一旦你选择了两者中的任何一个,那就坚持下去吧!

答案 22 :(得分:0)

好吧,如果我们假设添加/子路径操作成本为1,那么将成本乘以5,并将成本除以20。

答案 23 :(得分:0)

经过这么长时间的有趣讨论后,我对此表示了看法:这个问题没有最终答案。正如一些人所指出的那样,它取决于硬件(cf piotrkgast128)和编译器(cf @Javier的测试)。如果速度并不重要,如果您的应用程序不需要实时处理大量数据,您可以选择使用除法清晰度,而如果处理速度或处理器负载是一个问题,乘法可能是最安全的。 最后,除非您确切知道应用程序将部署在哪个平台上,否则基准测试毫无意义。为了清晰代码,单个评论就可以完成工作!

答案 24 :(得分:-2)

从技术上讲,没有除法这样的东西,只有乘以逆元素。例如,你永远不会除以2,实际上你乘以0.5。

'分裂' - 让我们自己说它存在一秒钟 - 总是更难以乘法,因为要xy首先需要计算值y^{-1}这样y*y^{-1} = 1然后进行乘法x*y^{-1}。如果您已经知道y^{-1},那么不从y计算它必须是优化。