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

Edm*_*ito 113 performance programming-languages

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

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

y = x / 2.0;
// or...
y = x * 0.5;
Run Code Online (Sandbox Code Playgroud)

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

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

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

Jav*_*ier 74

蟒蛇:

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
Run Code Online (Sandbox Code Playgroud)

乘法快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
Run Code Online (Sandbox Code Playgroud)

=>没有真正的区别

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
Run Code Online (Sandbox Code Playgroud)

=>它只快5%

结论:在Python中,乘法比分割更快,但随着使用更高级的VM或JIT越来越接近CPU,优势就消失了.未来的Python VM很可能会使它变得无关紧要

  • @rasmus:随着JIT越来越好,即使你要求分割,它也更有可能使用CPU乘法指令. (6认同)
  • 你的结论是错误的.随着JIT/VM变得更好,它变得更加相关.与VM的较低开销相比,分区变慢.请记住,编译器通常不能很好地优化浮点数以保证精度. (2认同)

Bil*_*l K 65

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

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

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

    • 编码未经优化
    • 确定"足够快"的速度 - 注意哪个用户要求/故事需要该指标.
    • 写一个速度测试
    • 测试现有代码 - 如果它足够快,你就完成了.
    • 重新编码优化
    • 测试优化代码.如果它不符合指标,请扔掉并保留原始指标.
    • 如果符合测试,请将原始代码保留为注释

此外,在不需要时删除内部循环或在数组上为插入排序选择链接列表等操作不是优化,只是编程.

  • 这不是完整的Knuth引用; 见http://en.wikipedia.org/wiki/Optimization_(computer_science)#When_to_optimize (7认同)
  • 真的有那么迷惑你吗?始终应用规则 1 和 2,除非您实际上不符合客户规范并且非常熟悉整个系统,包括 CPU 的语言和缓存特性。这时候,只需要按照 3 中的过程,不要只是想“嘿,如果我在本地缓存这个变量而不是调用 getter,事情可能会更快。首先证明它不够快,然后分别测试每个优化和扔掉那些没有帮助的。一路上大量记录。 (2认同)

Tho*_*ens 47

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

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

  • 特别是因为OP正在寻找Python的答案.我怀疑需要用Python写的那么多效率的东西. (27认同)
  • 划分可能是三角交叉例程中最昂贵的操作,这是大多数光线追踪器的基础.如果存储倒数和乘法而不是除数,您将经历多次加速. (4认同)

Mar*_*som 37

乘法更快,除法更准确.如果你的数字不是2的幂,你会失去一些精确度:

y = x / 3.0;
y = x * 0.333333;  // how many 3's should there be, and how will the compiler round?
Run Code Online (Sandbox Code Playgroud)

即使你让编译器找出反转常数到完美精度,答案仍然可能不同.

x = 100.0;
x / 3.0 == x * (1.0/3.0)  // is false in the test I just performed
Run Code Online (Sandbox Code Playgroud)

速度问题只有在C/C++或JIT语言中才有意义,即使这样,操作只是在一个瓶颈的循环中.

  • @ S.Lott:不,那不是真的.所有符合IEEE-754标准的浮点实现必须相对于当前舍入模式完美地舍入每个操作的结果(即,到最近的浮点数).乘以倒数总是会引入更多错误,至少因为必须再进行一次舍入. (8认同)
  • 带分母>分子的浮点除法必须在低位中引入无意义的值; 分裂通常会降低准确性 (7认同)
  • @JasonS 我刚刚让一个程序运行了一整夜,从 1.0 开始,按 1 ULP 递增;我比较了乘以“(1.0/3.0)”和除以“3.0”的结果。我得到了 1.0000036666774155,在这个空间中,7.3% 的结果是不同的。我认为它们仅相差 1 位,但由于 IEEE 算术保证四舍五入到最接近的正确结果,我坚持我的说法,即除法更准确。差异是否显着取决于您。 (3认同)

Jas*_*n S 24

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

y = x * (1.0 / 2.0);
Run Code Online (Sandbox Code Playgroud)

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

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

  • 也许你没有抓住重点。它与代数正确性无关。在理想的世界中,您应该只能除以 2:`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;”。 (12认同)
  • 适合自己(无论是谁) - 这是嵌入式世界的标准做法,该领域的软件工程师发现它很清楚. (11认同)
  • +1是唯一一个认识到编译器不能优化浮点运算但他们想要的人.它们甚至不能在乘法中改变操作数的顺序以保证精度(除非它使用松弛模式). (4认同)
  • 这真的很清楚为什么使用你的方法更清晰(和更准确). (3认同)

Car*_*ers 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;

}
Run Code Online (Sandbox Code Playgroud)

用.编译 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)
Run Code Online (Sandbox Code Playgroud)

和乘以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)
Run Code Online (Sandbox Code Playgroud)

但是,当我将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)
Run Code Online (Sandbox Code Playgroud)

乘法:

fstpl   -16(%ebp)
fldl    -16(%ebp)
fmulp   %st, %st(1)
fstpl   -16(%ebp)
Run Code Online (Sandbox Code Playgroud)

我没有对这些代码进行基准测试,但只是通过检查代码,你可以看到使用整数,除以2比乘以2短.使用双精度,乘法更短,因为编译器使用处理器的浮点操作码,可能运行得更快(但实际上我不知道)比不使用它们进行相同的操作.因此,最终这个答案表明多平面的性能为0.5而除以2则取决于语言的实现及其运行的平台.最终差异可以忽略不计,除了可读性之外,你几乎从不担心.

作为旁注,你可以看到我的程序main()返回a + b.当我拿走volatile关键字时,你永远不会猜到程序集的样子(不包括程序设置):

## 5/2

## 5*0.5
## done

movl    $5, %eax
leave
ret
Run Code Online (Sandbox Code Playgroud)

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

对不起,答案太长了.

  • @kvanberendonck当然这只是一条指令.算一下:`movl $ 5,%eax`优化的名称并不重要甚至不相关.你只想在一个四年前的答案中居高临下. (5认同)
  • 优化的本质对于理解仍然很重要,因为它是上下文敏感的:它仅适用于您添加/乘法/除/等的情况.编译时常量,编译器可以提前完成所有数学运算,并在运行时将最终答案移动到寄存器中.在一般情况下(运算符除数),除法比乘法慢很多,但我认为乘以倒数只有在你以其他方式除以相同分母时才有效.你可能知道这一切,但是新的程序员可能需要拼写出来,所以...以防万一. (2认同)

Jam*_*sta 10

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

如果你从一个非常高的级别讲话​​,对于任何你可能使用它的东西,它都不会慢得多.你会在其他答案中看到,人们需要做一百万乘法/除法才能测量两者之间的一些亚毫秒差异.

从低级优化的角度来看,如果你还是好奇的话:

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

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

一个类比是在观看电视节目时将馅饼放在微波炉中.你离开电视节目的总时间是将它放入微波炉并将其从微波炉中取出多长时间.剩下的时间你还在观看电视节目.因此,如果馅饼花了10分钟烹饪而不是1分钟,它实际上并没有消耗掉你的电视观看时间.

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

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


Jay*_*uzi 7

写下哪一个更清楚地表明你的意图.

程序运行后,弄清楚什么是缓慢的,并使速度更快.

不要反过来做.


but*_*oxa 6

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

让编译器为您执行性能.


Gen*_*ene 6

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


Too*_*the 2

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