我应该用乘法还是除法?

这里有一个有趣的傻问题:

假设我们必须执行一个简单的操作,其中我们需要变量值的一半。一般来说有两种做法:

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

假设我们使用的是语言提供的标准运算符,那么哪个运算符的性能更好呢?

我猜乘法通常更好,所以我尝试坚持,当我编码,但我想确认这一点。

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

67702 次浏览

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

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

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

乘法通常更快——当然绝不会更慢。 但是,如果不是速度关键,请写出最清晰的代码。

我认为这是如此吹毛求疵,您最好做任何使代码更具可读性的事情。除非你执行数千次,甚至数百万次的操作,否则我怀疑没有人会注意到其中的差别。

如果你真的不得不做出选择,基准测试是唯一的出路。找出哪些函数出现了问题,然后找出问题出现在函数的哪些部分,并修复这些部分。然而,我仍然怀疑一个单一的数学运算(即使是一个重复很多很多次的运算)是造成瓶颈的原因。

我在哪里读到过,在 C/C + + 中乘法效率更高; 对于解释语言一无所知——由于所有其他开销,这种差异可能是可以忽略不计的。

除非它成为一个问题,坚持什么是更可维护/可读-我讨厌它,当人们告诉我这一点,但它是如此真实。

那么,如果我们假设一个添加/子跟踪操作的成本为1,那么乘以成本为5,除以成本约为20。

乘法更快,除法更精确。如果你的数字不是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 语言中,速度问题才有可能发生,即使在这种情况下,也只有当操作处于瓶颈的循环中时才会发生。

你想干什么都行。首先想想你的读者,不要担心性能,直到你确定你有一个性能问题。

让编译器为您完成性能。

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

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

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

巨蟒:

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

= > 没有实质区别

卢阿吉特:

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 时,优势就消失了。未来的 PythonVM 很可能会使它变得无关紧要

写出你的意图。

在程序运行之后,找出哪些地方比较慢,然后加快速度。

别反过来。

总是使用最清楚的东西。你所做的一切都是为了智胜编译器。如果编译器足够聪明,它会尽最大努力优化结果,但没有什么能让下一个家伙不因为你蹩脚的位移解决方案而恨你(顺便说一句,我喜欢位操作,它很有趣。但是很有趣!= 可读)

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

  1. 不要优化。
  2. 如果您是专家,请参见规则 # 1
  3. 如果你是一个专家,并且能够证明有这个需要,那么使用以下程序:

    • 未经优化的代码
    • 确定“足够快”有多快——注意哪个用户需求/故事需要这个度量。
    • 写一个速度测试
    • 测试现有代码——如果速度足够快,那么您就完成了。
    • 重新编码优化
    • 测试经过优化的代码。如果它不符合标准,扔掉它,保留原始代码。
    • 如果满足测试要求,则将原始代码保留为注释

此外,在不需要内部循环时删除它们,或者在插入排序时选择链表而不是数组,这些都不是优化,只是编程。

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

要小心“猜测乘法通常更好,所以我在编码时尽量坚持这一点。”

在这个具体问题的上下文中,better 在这里的意思是“更快”,这并不是很有用。

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

参见 带误差分析的浮点算法。参见 浮点运算的基本问题及误差分析

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

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

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

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

如果你想优化你的代码,但仍然是清晰的,尝试这样:

y = x * (1.0 / 2.0);

编译器应该能够在编译时进行除法,所以在运行时会得到一个乘法。我希望精确度与 y = x / 2.0的情况相同。

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

只是要为“其他语言”选项添加一些东西。
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 可能会这么做)时,我得到了以下结果:

部门:

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

它在一条指令中同时做除法、乘法和加法运算!显然,如果优化器值得尊敬,您就不必担心这个问题。

抱歉回答得太长了。

与第24条(乘法更快)和第30条一样——但有时它们也同样容易理解:

1*1e-6F;


1/1e6F;

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

Java 机器人,配置为三星 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%

技术上没有除法这回事,只有逆元素的乘法。例如,你从来没有除以2,你实际上乘以0.5。

“除法”——让我们自欺欺人地认为它存在一秒钟——总是比乘法更难,因为要把 x除以 y,首先需要计算 y^{-1}的值,这样 y*y^{-1} = 1就可以做到,然后再乘 x*y^{-1}。如果您已经知道 y^{-1},那么不从 y计算它必须是一个优化。

这是有区别的,但它是依赖于编译器的。首先,在 vs2003(c + +)上,对于双类型(64位浮点数) ,我没有得到显著差异。然而,在 vs2010上再次运行测试时,我发现了一个巨大的差异,对于乘法运算,最高可以快4倍。跟踪这一点,vs2003和 vs2010似乎生成了不同的 fpu 代码。

在 Pentium 4,2.8 GHz,vs2003上:

  • 乘法: 8.09
  • 分数: 7.97

至强 W3530 Vs2003:

  • 乘法是4.68
  • 分数: 4.64

至强 W3530 Vs2010:

  • 乘法: 5.33
  • 分区: 21.05

似乎在 vs2003中,循环中的除法(因此除数被多次使用)被转换为带有反数的乘法。在 vs2010上,这种优化不再应用(我认为这是因为两种方法的结果略有不同)。还要注意,当分子为0.0时,CPU 执行除法的速度会更快。我不知道精确的算法硬连接在芯片,但也许它是数字依赖。

编辑18-03-2013: 对2010年的观察

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

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

首先,除非你使用的是 C 语言或者汇编语言,否则你可能使用的是一种更高级的语言,在这种语言中,内存停顿和一般的调用开销绝对会使乘法和除法之间的差异缩小到无关紧要的程度。所以,在这种情况下选择读起来更好的。

如果你是从一个非常高的水平谈话,它不会测量慢任何你可能使用它。你会在其他答案中看到,人们需要做100万次乘除才能测量出两者之间亚毫秒级的差异。

如果你仍然好奇,从低水平优化的角度来看:

除法比乘法的管道长得多。这意味着需要更长的时间才能得到结果,但是如果您可以让处理器忙于处理非依赖性任务,那么最终的成本不会超过一个乘法。

管道差的长度完全取决于硬件。我使用的最后一个硬件是类似于9个周期的 FPU 乘法和50个周期的 FPU 除法。听起来很多,但是你会因为记忆缺失而损失1000个周期,所以这可以让事情变得正确。

比方说,当你看电视节目的时候,把一个馅饼放进微波炉里。你离开电视节目的总时间就是把它放进微波炉,再从微波炉里拿出来的时间。剩下的时间你还在看电视节目。所以,如果馅饼用了10分钟而不是1分钟来做,它实际上并没有占用你看电视的时间。

在实践中,如果要了解 Multily 和 Divide 之间的区别,就需要了解管道、缓存、分支延迟、无序预测和管道依赖关系。如果这个问题听起来不像是你想要问的,那么正确的答案是忽略这两者之间的区别。

许多(许多)年前,避免除法和总是使用乘法是绝对重要的,但那时内存命中相关性较低,除法更糟糕。现在我对可读性的评价更高,但如果没有可读性差异,我认为选择乘法是一个好习惯。

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

X/2.0 等于 没有 < strong > x * 0.5

假设您在2008年10月22日编写了这个方法。

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

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

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

修改密码是正确的决定吗?在数学中,这两个表达式确实是等价的。在计算机科学中,这并不总是正确的。详情请参阅 最小化精度问题的影响。如果您的计算值在某个时候与其他值进行比较,您将更改边缘情况的结果。例如:

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

底线是: 一旦你满足于这两者中的任何一个,那么就坚持下去!