这里有一个有趣的问题:
假设我们需要执行一个简单操作,其中需要使用变量值的一半。通常有两种方法实现:
y = x / 2.0;
// or...
y = x * 0.5;
假设我们使用语言提供的标准运算符,哪个运算符性能更好?
我猜乘法通常更好,所以我在编码时尽量使用它,但我想确认一下。
虽然我个人对Python 2.4-2.5的答案感兴趣,但也可以为其他语言发布答案!如果您愿意,也可以发布其他更高级的方法(如使用位移运算符)。
这里有一个有趣的问题:
假设我们需要执行一个简单操作,其中需要使用变量值的一半。通常有两种方法实现:
y = x / 2.0;
// or...
y = x * 0.5;
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中,乘法比除法更快,但随着使用更先进的虚拟机或JITs靠近CPU,优势会消失。未来的Python虚拟机可能会使其变得不相关。
始终使用最清晰的方法,其他方法都是试图比编译器更聪明。如果编译器有一点智能,它会尽最大努力优化结果,但没有什么能阻止下一个人因为你的可怜的位操作解决方案而憎恨你 (我顺便说一下,位操作很有趣,但有趣 ≠ 可读)
过早地进行优化是万恶之源,永远记住优化的三个规则!
如果您是专家并且可以证明需要进行优化,则应使用以下流程:
另外,像在不需要的情况下删除内部循环或选择链接列表进行插入排序而不是数组等操作并非优化,而只是编程。
我认为这变得太过吹毛求疵,你最好做任何可以让代码更易读的事情。除非你执行操作数以千计甚至百万计,否则我怀疑任何人都不会注意到差异。
如果你真的必须做出选择,基准测试是唯一的方法。找出哪些函数给你带来了问题,然后找出在函数中哪些部分出现了问题,并修复这些部分。然而,我仍然怀疑单个数学运算(即使重复多次)也不会成为任何瓶颈的原因。
乘法更快,除法更准确。如果你的数字不是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中不能完美地表示,但当你执行浮点运算时,无论是乘法还是除法,你都会失去精度,因为低位的位会被舍入。如果你知道你的计算对舍入误差如此敏感,你也应该知道如何最好地处理这个问题。 - Jason S(1.0/3.0)
的结果与除以3.0
的结果进行了比较。我一直比较到1.0000036666774155,在这个范围内有7.3%的结果是不同的。我认为它们只是相差1位,但由于IEEE算术保证四舍五入到最接近的正确结果,所以我坚持认为除法更准确。这种差异是否显著取决于你。 - Mark Ransom如果你想优化你的代码并且仍然保持清晰易懂,可以尝试这个方法:
y = x * (1.0 / 2.0);
编译器应该能够在编译时进行除法运算,因此您将在运行时获得乘法运算。我期望精度与 y = x / 2.0
的情况相同。
这在需要使用浮点数计算时需要进行浮点数模拟的嵌入式处理器中可能非常重要。
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;
。 - Jason S2.0
是2的幂,因此它的倒数0.5
可以精确地表示为浮点数或双精度浮点数。请参见为什么编译器不将“n / 2.0”强制转换为“n * 0.5”,如果速度更快?作为一个例子(如果启用优化,则会执行此操作,如答案所示)。对于不能精确表示其倒数的除数,您必须手动执行此操作或使用-ffast-math
以获得速度。 - Peter Cordes我来为“其他语言”选项再添加一些内容。
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可能会做的),我得到了这个结果:
division:
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的性能取决于编程语言和平台的实现。最终的差异是微不足道的,你几乎永远不需要担心它,除非从可读性的角度来考虑。
另外一件事,当我去掉volatile
关键字时,你可能无法猜出我的程序main()
返回的汇编代码长什么样子(排除了程序设置):
## 5/2
## 5*0.5
## done
movl $5, %eax
leave
ret
它在一条指令中完成了除法、乘法和加法!如果优化器足够尊重,你就不必担心这个问题。
对于过长的回答表示抱歉。
movl $5, %eax
优化的名称并不重要,甚至与此无关。你只是想在一个四年前的答案上表现得居高临下。 - Carson Myers-O1
?另外,更好的查看汇编代码的方法是编写一个带参数并返回值的函数。如果你只是查看汇编代码,就不需要或者不想要一个main
函数。(特别是在GCC中,你需要避免使用main
函数,因为它对于main
有一个隐式的__attribute__((cold))
属性。)但是,将整数乘以0.5
是完全错误的,特别是在旧版x87中,从浮点数到整数的截断非常不方便,必须改变舍入模式。 - Peter Cordes写出更清晰地表达您意图的方式。
在程序能够运行后,找出哪些部分速度较慢,并让它们变得更快。
不要反过来做。
尽管你需要的任何操作。首先考虑你的读者,不要担心性能问题,直到你确定存在性能问题。
让编译器为你处理性能问题。
0.5 * x
或 1.0/2.0 * x
而不是 x / 2.0
是合理的。 "编写清晰代码" 的严谨性绝对正确,但这三种方法在可读性上非常接近,所以在这种情况下,严谨性只是纠结。