关于性能:我应该使用乘法还是除法?

您所在的位置:网站首页 求积是乘法还是除法 关于性能:我应该使用乘法还是除法?

关于性能:我应该使用乘法还是除法?

2024-07-12 19:15| 来源: 网络整理| 查看: 265

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

假设我们必须执行一个简单的操作,其中我们需要变量值的一半。 通常有两种方法可以执行此操作:

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

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

我猜想乘法通常更好,所以我在编码时会尽量坚持下去,但是我想确认一下。

尽管我个人对Python 2.4-2.5的答案很感兴趣,但是也可以发布其他语言的答案! 而且,如果您愿意,也可以发布其他更奇特的方式(例如使用按位移位运算符)。

相关讨论 您是否进行了基准测试?它只有十几行代码。您从运行基准测试中学到了什么? [提示:这样做比在这里发布问题要快。] IPython中的timeit命令是一行 伟大的问题,已经引起了一些非常有趣的答案/讨论。谢谢 :) 请参见stackoverflow.com/questions/20263050/,以获取有关VB.NET关于基本相同问题的讨论以及基准测试。 即使他已经通过基准测试了解到了答案,它仍然是一个有用的问题,并且产生了一些有趣且有用的答案。我也希望人们坚持这一点,不要对答案写答案和评论,就是否值得进行优化建议提供不相关的建议。为什么不假设OP在按书面形式询问问题,而不是假设他或她确实希望大规模地改写建议。 除法比乘法慢得多。但是某些智能编译器/虚拟机将除法转换为乘法,因此您的测试将具有相同的结果(两个测试均测试乘法)。 话题有点离题,但我只想说说我对@KevinWhitefoot的认同。没有什么比阅读讲道者更令人沮丧的了,而没有对技术问题的技术性回答。感谢Kevin的评论!

Python:

123456789time 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%

卢阿:

123456789time 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:

123456789time 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很可能会使其变得无关紧要

相关讨论 感谢您使用time命令进行基准测试的技巧! 您的结论是错误的。随着JIT / VM变得更好,它变得越来越重要。与VM的较低开销相比,该划分变得更慢。请记住,为了保证精度,编译器通常不能对浮点进行太多优化。 @rasmus:随着JIT的改进,即使您要求除法,也更有可能使用CPU乘法指令。

始终使用最清晰的东西。您所做的任何其他操作都试图使编译器的性能超越智能。如果编译器是完全智能的,它将尽最大努力优化结果,但是没有什么可以使下一个男人不讨厌您讨厌的位移解决方案(顺便说一下,我喜欢位操作,很有趣。但是很有趣!=可读) )

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

不要优化。 如果您是专家,请参阅规则1

如果您是专家并且可以证明需要,那么请使用以下过程:

对其进行未优化的编码 确定"足够快"的速度-注意哪个用户要求/故事需要该指标。 编写速度测试 测试现有代码-如果速度足够快,就可以完成。 重新编码优化 测试优化的代码。如果不符合指标,则将其丢弃并保留原始指标。 如果符合测试要求,请保留原始代码作为注释

同样,执行诸如在不需要时删除内部循环或在数组中为插入排序选择链表之类的操作也不是优化,而只是编程。

相关讨论 那不是克努斯的全部报价;参见en.wikipedia.org/wiki/ 不,来自许多不同来源的主题大约有40种不同的引用。我有点凑在一起。 您的最后一句话不清楚何时应用规则#1和#2,从而使我们回到了开始的地方:我们需要确定哪些优化值得,哪些优化不值得。假装答案很明显不是答案。 真的让您感到困惑吗?除非您实际上不符合客户端规范并且对整个系统非常熟悉,包括CPU的语言和缓存特性,否则请始终应用规则1和2。那时,仅遵循3中的步骤,不要以为"嘿,如果我在本地缓存此变量而不是调用getter,事情可能会更快。首先证明它不够快,然后分别测试每个优化并抛出。那些无济于事的文件。

我认为这变得太挑剔了,您最好做任何使代码更具可读性的事情。除非您执行该操作数千次(甚至数百万次),否则我怀疑有人会注意到这种差异。

如果您真的必须做出选择,则基准测试是唯一的选择。查找哪些功能给您带来了问题,然后找出功能中出现问题的位置,并修复这些部分。但是,我仍然怀疑单个数学运算(甚至重复多次,多次)是否会引起任何瓶颈。

相关讨论 当我过去制造雷达处理器时,一次操作确实有所作为。但是,我们正在手动优化机器代码以实现实时性能。对于其他所有事情,我都赞成简单明了。 我猜对于某些事情,您可能只关心一个操作。但是我希望在99%的应用程序中都没关系。 特别是因为OP在Python中寻找答案。我怀疑任何需要达到这种效率的东西都会用Python编写。 在三角相交例程中,除法可能是最昂贵的操作,这是大多数光线跟踪器的基础。如果存储倒数并乘而不是除,则将经历很多倍的加速。 @solinent-是的,可以提速,但我怀疑"很多次"-浮点数除法和乘法的差异不应超过4:1,除非所讨论的处理器确实真正针对乘法而不是除法进行了优化。 @JasonS 4:1听起来对我来说是非常有意义的进步。"什么?您刚刚使游戏从15 fps变为60 fps?这无关紧要。您被解雇了!!!" 我同意这一点,我只是认为4:1不是"很多次"。是的,值得做,但是要准确。

乘法更快,除法更准确。如果您的数字不是2的幂,则将失去一些精度。

12y = x / 3.0; y = x * 0.333333;  // how many 3's should there be, and how will the compiler round?

即使让编译器找出倒数常量以达到最佳精度,答案也可能有所不同。

12x = 100.0; x / 3.0 == x * (1.0/3.0)  // is false in the test I just performed

速度问题仅可能在C / C ++或JIT语言中才有关系,即使只有在瓶颈处循环运行的情况下,速度问题才算重要。

相关讨论 如果您用整数除法,除法是准确的。 分母>分子的浮点除法必须在低阶位引入无意义的值;除法通常会降低准确性。 @ S.Lott:不,那不是真的。所有符合IEEE-754的浮点实现必须相对于当前舍入模式将每个运算的结果完美舍入(即至最接近的浮点数)。乘以倒数总是会引入更多的误差,至少是因为必须再进行一次舍入。 我知道这个答案已有8年之久了,但它具有误导性。您可以执行除法而不会显着降低精度:y = x * (1.03.0);,编译器通常会在编译时计算1/3。是的,在IEEE-754中1/3不能完全代表,但是当您执行浮点算术时,无论您是进行乘法还是除法,无论如何您都会失去精度,因为低位是四舍五入的。如果您知道您的计算对舍入误差非常敏感,那么您还应该知道如何最好地处理该问题。 @JasonS我刚刚让程序运行了一整夜,从1.0开始并以1 ULP递增;我比较了乘以(1.03.0)和除以3.0的结果。我的分数高达1.0000036666774155,在那个空间中7.3%的结果有所不同。我认为它们之间只有1位的差异,但是由于IEEE算术可以保证四舍五入到最接近的正确结果,因此我坚持说除法更准确。差异是否显着取决于您。 @JasonS对于失败的示例,请尝试1.0009765625。

如果您想优化代码但仍要保持清晰,请尝试以下操作:

1y = x * (1.0 / 2.0);

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

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

相关讨论 该代码根本不清楚。 适合您自己(以及-1d的任何人)-这是嵌入式领域的标准做法,该领域的软件工程师认为这很清楚。 呵呵,今天又是-1,没有任何评论。 +1是唯一的实现者,意识到编译器无法根据需要优化浮点运算。为了保证精度,它们甚至不能更改操作数的顺序(除非使用宽松模式)。 OMG,至少有6位程序员认为基本数学尚不清楚。 AFAIK,IEEE 754乘法是可交换的(但不相关)。 也许您错过了重点。它与代数正确性无关。在理想的世界中,您应该只能将其除以二: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;。 这确实很清楚为什么使用您的方法更清晰(更精确)。

只是要为"其他语言"选项添加一些内容。 C:由于这只是一项学术活动,实际上没有任何区别,所以我想我会有所作为。

我没有进行任何优化就编译为汇编,并查看了结果。 编码:

12345678910111213141516171819int main() {     volatile int a;     volatile int b;     asm("## 5/2 ");     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:

1234567movl    $5, -4(%ebp) movl    -4(%ebp), %eax movl    %eax, %edx shrl    $31, %edx addl    %edx, %eax sarl    %eax movl    %eax, -4(%ebp)

乘以0.5:

123456789101112131415movl    $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可能要做的)时,我得到了:

师:

12345678flds    LC0 fstl    -8(%ebp) fldl    -8(%ebp) flds    LC1 fmul    %st, %st(1) fxch    %st(1) fstpl   -8(%ebp) fxch    %st(1)

乘法:

1234fstpl   -16(%ebp) fldl    -16(%ebp) fmulp   %st, %st(1) fstpl   -16(%ebp)

我没有对任何代码进行基准测试,但是仅通过检查代码就可以看到,使用整数,除以2的时间比乘以2的时间短。使用双精度,乘法的时间更短,因为编译器使用处理器的浮点操作码,即可能比不使用它们进行相同的操作运行得更快(但实际上我不知道)。因此,最终的答案表明,乘以0.5与除以2的性能取决于语言的实现及其运行的平台。最终,差异几乎可以忽略不计,除了可读性以外,您几乎永远不必担心。

作为附带说明,您可以看到在我的程序中main()返回a + b。当我拿掉volatile关键字时,您将永远不会猜出程序集的样子(不包括程序设置):

12345678## 5/2 ## 5*0.5 ## done movl    $5, %eax leave ret

它在一条指令中完成了除法,乘法和加法运算!显然,如果优化程序是任何受人尊敬的,您都不必为此担心。

抱歉,答案太长了。

相关讨论 它不是一个"单一指令"。它只是不断折叠。 @kvanberendonck当然是一条指令。指望它们:movl $5, %eax优化的名称并不重要,甚至不相关。您只是想屈服于四年的答案。 优化的性质仍然很重要,因为它与上下文有关:它仅在您添加/相乘/除法等情况下适用。编译时常量,编译器可以提前完成所有数学运算,然后在运行时将最终答案移至寄存器中。在一般情况下(运行时间除数),除法比乘法慢很多,但是我想乘以倒数仅在您用相同的分母除以除法一次以上的情况下才有用。您可能知道所有这些,但是较新的程序员可能需要详细说明,以防万一。

首先,除非您使用C或ASSEMBLY进行工作,否则您可能使用的是高级语言,其中内存停滞和常规调用开销将使相乘和相除之间的差异绝对相形见to。因此,只需选择在这种情况下更好的方法即可。

如果您是从很高的级别进行交谈,那么您可能会用它来衡量任何事情的速度都不会太慢。您还会在其他答案中看到,人们需要做一百万次乘/除,才能测量两者之间的亚毫秒差异。

如果您仍然好奇,请从低级优化的角度来看:

除法往往具有比乘积更长的流水线。这意味着要花费更长的时间才能得到结果,但是如果您可以让处理器忙于执行非相关任务,那么最终花费的成本不会超过乘法运算。

流水线差异有多长完全取决于硬件。我使用的最后一个硬件是FPU乘法的9个周期和FPU除法的50个周期。听起来很多,但随后您将因内存丢失而丢失1000个周期,因此可以将其视为现实。

打个比方,就是在看电视节目时将馅饼放在微波炉里。使您离开电视节目的总时间是将其放入微波炉中并从微波炉中取出要花费多长时间。剩下的时间,您仍然看电视节目。因此,如果该饼花了10分钟而不是1分钟来煮,那么它实际上并没有用完电视上的观看时间。

在实践中,如果您要关注乘法和除法之间的差异,则需要了解管道,缓存,分支停顿,无序预测和管道依赖性。如果这听起来不像您打算解决的问题,那么正确的答案就是忽略两者之间的区别。

许多(很多)年前,绝对重要的是要避免使用除法,并且始终使用乘法,但是在那时,内存命中率就不那么重要了,除法更糟。如今,我对可读性的评价更高,但是如果没有可读性差异,我认为选择乘数是一个好习惯。

写下更清晰的陈述您的意图。

程序运行后,找出缓慢的地方,并加快速度。

不要反过来做。

做您需要的任何事情。首先考虑您的读者,在确定性能问题之前,不要担心性能。

让编译器为您完成性能。

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

12345    int y = 10;     y = y >> 1;     Console.WriteLine("value halved:" + y);     y = y >> 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

在此示例中,您可以看到,当值变小时,几乎相等的数字之间的差会产生非零结果,其中正确答案为零。

有所不同,但这取决于编译器。最初在vs2003(c ++)上,我对双精度类型(64位浮点数)没有明显的不同。但是在vs2010上再次运行测试时,我发现了巨大的差异,乘法运算的速度提高了4倍。追根溯源,似乎vs2003和vs2010会生成不同的fpu代码。

在奔腾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的观察

相关讨论 我想知道是否有任何原因编译器无法替换例如n10.0的形式为(n * c1 + n * c2)的表达式?我希望在大多数处理器上,一个除法运算将花费比两个乘法和一个除法运算更长的时间,并且我相信,在任何情况下,使用所示公式进行除以任何常数都可以得出正确舍入的结果。

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

1231*1e-6F; 1/1e6F;

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

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

x / 2.0不等于x * 0.5

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

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

十年后的今天,您了解到可以优化这段代码。 在整个应用程序中,数百种公式中都引用了该方法。 因此,您对其进行了更改,并获得了显着的5%性能提升。

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

更改代码是正确的决定吗? 在数学中,这两个表达式的确相等。 在计算机科学中,并不总是如此。 有关更多详细信息,请阅读最小化准确性问题的影响。 如果您的计算值在某些时候与其他值进行了比较,则将更改边缘工况的结果。 例如。:

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

底线是; 一旦您解决了这两个问题中的任何一个,那就坚持下去吧!

相关讨论 下注?解释您的想法的评论怎么样?这个答案绝对是100%相关的。 在计算机科学中,除非浮点值变得非正规化或溢出,否则乘以2的乘方/除法是无损的。 由于除法时的浮点数不是无损的,因此您的陈述是否正确并不重要。虽然我会很惊讶。 仅当您使用发出不推荐使用的x87代码的古老编译器进行构建时,"浮点在划分时并非无损"。在现代硬件上,只有float / double变量是无损的,无论是32位还是64位的IEEE 754:en.wikipedia.org/wiki/IEEE_754由于IEEE 754的工作方式,当您除以2或乘以0.5时,您会减少指数乘以1,其余位(符号+尾数)不变。而且2和0.5数字都可以在IEEE 754中准确表示,而不会损失任何精度(与0.4或0.1不同,它们不能)。

我读过某个地方,乘法在C / C ++中更有效。没有关于解释语言的想法-由于其他所有开销,差异可能微不足道。

除非它成为一个问题,否则请坚持更可维护/更易理解的内容-当人们告诉我这是事实时,我讨厌它。

在Samsung GT-S5830上分析的Java android

123456789101112131415161718public void Mutiplication() {     float a = 1.0f;     for(int i=0; i


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3