@Dunes的这个answer表示,由于流水线,浮点乘法和除法之间几乎没有差别。但是,从我对其他语言的看法来看,我认为这种划分会更慢。
我的小测试看起来如下:
A=np.random.rand(size)
command(A)
对于不同的命令和size=1e8
我在我的机器上得到以下时间:
Command: Time[in sec]:
A/=0.5 2.88435101509
A/=0.51 5.22591209412
A*=2.0 1.1831600666
A*2.0 3.44263911247 //not in-place, more cache misses?
A+=A 1.2827270031
最有趣的部分:除以0.5
几乎是除以0.51
的两倍。可以假设,这是由于一些智能优化,例如用A+A
替换除法。但是,A*2
和A+A
的时间安排距离太远,无法支持此声明。
通常,使用值为(1/2)^n
的浮点值进行划分的速度更快:
Size: 1e8
Command: Time[in sec]:
A/=0.5 2.85750007629
A/=0.25 2.91607499123
A/=0.125 2.89376401901
A/=2.0 2.84901714325
A/=4.0 2.84493684769
A/=3.0 5.00480890274
A/=0.75 5.0354950428
A/=0.51 5.05687212944
如果我们查看size=1e4
:
Command: 1e4*Time[in sec]:
A/=0.5 3.37723994255
A/=0.51 3.42854404449
A*=2.0 1.1587908268
A*2.0 1.19793796539
A+=A 1.11329007149
现在,除.5
和.51
之间没有区别!
我尝试了不同的numpy版本和不同的机器。在某些机器上(例如Intel Xeon E5-2620)可以看到这种效果,但在其他一些机器上却看不到 - 这不依赖于numpy版本。
使用@Ralph Versteegen的剧本(请参阅他的答案!)我得到以下结果:
问题是:如果数组大小较大(某些处理器除以0.51
与0.5
相比,除法成本较高的原因是什么?大于10 ^ 6)。
最初的问题是“如何解释这些不同的行为(按0.5
除以除0.51
)?”
这里也是我原来的测试脚本,它产生了时间:
import numpy as np
import timeit
def timeit_command( command, rep):
print "\t"+command+"\t\t", min(timeit.repeat("for i in xrange(%d):"
%rep+command, "from __main__ import A", number=7))
sizes=[1e8, 1e4]
reps=[1, 1e4]
commands=["A/=0.5", "A/=0.51", "A*=2.2", "A*=2.0", "A*2.2", "A*2.0",
"A+=A", "A+A"]
for size, rep in zip(sizes, reps):
A=np.random.rand(size)
print "Size:",size
for command in commands:
timeit_command(command, rep)
答案 0 :(得分:21)
起初我怀疑numpy正在调用BLAS,但至少在我的机器上(python 2.7.13,numpy 1.11.2,OpenBLAS),它没有,因为用gdb快速检查显示:
> gdb --args python timing.py
...
Size: 100000000.0
^C
Thread 1 "python" received signal SIGINT, Interrupt.
sse2_binary_scalar2_divide_DOUBLE (op=0x7fffb3aee010, ip1=0x7fffb3aee010, ip2=0x6fe2c0, n=100000000)
at numpy/core/src/umath/simd.inc.src:491
491 numpy/core/src/umath/simd.inc.src: No such file or directory.
(gdb) disass
...
0x00007fffe6ea6228 <+392>: movapd (%rsi,%rax,8),%xmm0
0x00007fffe6ea622d <+397>: divpd %xmm1,%xmm0
=> 0x00007fffe6ea6231 <+401>: movapd %xmm0,(%rdi,%rax,8)
...
(gdb) p $xmm1
$1 = {..., v2_double = {0.5, 0.5}, ...}
实际上,无论使用什么常量,numpy都运行完全相同的泛型循环。因此,所有时序差异完全归功于CPU。
实际上,除法是一种执行时间变化很大的指令。要完成的工作量取决于操作数的位模式,也可以检测和加速特殊情况。根据{{3}}(其准确性我不知道),在您的E5-2620(Sandy Bridge)上,DIVPD具有10-22个周期的延迟和反向吞吐量,而MULPS具有10个周期的延迟和反向吞吐量5个周期。
现在,A*2.0
慢于A*=2.0
。 gdb显示正在使用完全相同的函数进行乘法,但现在输出op
与第一个输入ip1
不同。因此,它必须纯粹是被吸入高速缓存的额外内存的工件,从而减慢大输入的非就地操作(即使MULPS每个周期仅产生2 * 8/5 = 3.2字节的输出!)。当使用1e4大小的缓冲区时,所有内容都适合缓存,因此不会产生显着影响,而其他开销大多会消除A/=0.5
和A/=0.51
之间的差异。
但是,在这些时间中有很多奇怪的效果,所以我绘制了一些图表(生成此代码的代码如下)
我根据每条DIVPD / MULPD / ADDPD指令的CPU周期数绘制了A数组的大小。我是在3.3GHz AMD FX-6100上运行的。黄色和红色垂直线是L2和L3缓存大小。根据这些表,1 / 4.5周期(看起来可疑),蓝线是DIVPD的假设最大吞吐量。正如你所看到的那样,即使{&#34; Overhead&#34;执行numpy操作接近于零。因此,大约有24个周期的开销只是循环,从L2缓存读取和写入16个字节!非常令人震惊,也许内存访问没有对齐。
需要注意很多有趣的效果:
A+=2.0
和A/=0.5
之间的速度差异在图表右侧下降;这是因为当读/写内存的时间增加时,它会重叠并掩盖进行除法所需的一些时间。因此,A/=0.51
,A/=0.5
和A*=2.0
的速度相同。A+=2.0
,A/=0.51
和A/=0.5
之间的最大差异表明,除法的吞吐量为4.5-44个周期,与Agner的4.5-11不匹配。 s table。不幸的是,在具有不同FPU速度,缓存大小,内存带宽,numpy版本等的CPU的另一台机器上,这些曲线可能看起来很不一样。
我从中得到的结论是:将多个算术运算与numpy链接在一起比在Cython中执行相同操作慢几倍,同时迭代输入只需一次,因为没有&#34;最佳点&# 34;算术运算的成本主导其他成本。
A+=2.0
答案 1 :(得分:15)
英特尔CPU在除以2的幂时进行了特殊优化。例如,请参阅http://www.agner.org/optimize/instruction_tables.pdf,其中说明了
FDIV延迟取决于控制字中指定的精度:64位精度 提供延迟38,53位精度提供延迟32,24位精度提供延迟18.除以2的幂需要9个时钟。
虽然这适用于FDIV而不是DIVPD(如@RalphVersteegen的回答说明),如果DIVPD也没有实施此优化,那将是相当令人惊讶的。
分工通常是非常缓慢的事情。然而,除以2的幂除以只是指数移位,并且尾数通常不需要改变。这使得操作非常快。此外,在浮点表示中很容易检测到2的幂,因为尾数将全为零(具有隐式前导1),因此这种优化既易于测试又便宜实现。