Python Numpy:除法0.5有什么特别之处?
@Dunes的这一部分指出,由于流水线运算,浮点乘法和除法(几乎)没有区别。然而,根据我在其他语言方面的经验,我认为分工会慢一些 我的小测试如下所示:Python Numpy:除法0.5有什么特别之处?,python,performance,numpy,Python,Performance,Numpy,@Dunes的这一部分指出,由于流水线运算,浮点乘法和除法(几乎)没有区别。然而,根据我在其他语言方面的经验,我认为分工会慢一些 我的小测试如下所示: 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
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的脚本(参见他的精彩答案!),我得到了以下结果:
- i5-2620的计时(Haswell,2x6内核,但非常旧的numpy版本,不使用SIMD):
- i7-5500U(Broadwell,2芯,numpy 1.11.2)的计时:
0.5
相比,除以0.51
的成本更高的原因是什么
@nneonneo的回答指出,对于某些英特尔处理器,当除以二的幂时,会有一个优化,但这并不能解释为什么我们只能看到它对大型阵列的好处
最初的问题是“如何解释这些不同的行为(按
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)
起初我怀疑numpy正在调用BLAS,但至少在我的机器(python 2.7.13、numpy 1.11.2、OpenBLAS)上,它没有调用BLAS,通过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}, ...}
英特尔CPU在除以二的幂时有特殊的优化。例如,请参见其中的说明
FDIV延迟取决于控制字中指定的精度:64位精度
给出延迟38,53位精度给出延迟32,24位精度给出延迟18。2的幂除法需要9个时钟
尽管这适用于FDIV而不是DIVPD(正如@RalphVersteegen的回答所述),但如果DIVPD也没有实现这种优化,那将是非常令人惊讶的
分裂通常是一件非常缓慢的事情。然而,除以二的幂只是一个指数移动,尾数通常不需要改变。这使得操作非常快。此外,在浮点表示法中很容易检测到二的幂,因为尾数都是零(带一个隐式的前导1),所以这种优化既易于测试,又易于实现。您的基准是什么?算术速度还是解释器效率?@Yves我的目标是对算术速度进行基准测试,但我不确定我的实际基准测试是什么。我无法重现0.5和0.51之间的除法差异。在IPython中使用%timeit
魔术似乎花费了相同的时间,不管数组大小如何。@Laleh这是整数值,浮点值也是如此吗?@ajcr在另一台具有较新硬件的机器上,我也看不到0.5
和0.51
之间的区别:(你是对的,这是使用的同一个代码,所以差异一定来自硬件。仍然有很多事情我不明白,例如,为什么0.5和0.51对于较小的尺寸没有区别?老实说,有很多东西我也不明白,所以我重新检查了它,并重写了我的答案的下半部分。我是我了解了一些事情,但不幸的是,我仍然不了解所有内容。这是一个很好的答案!我用2核机器上的测量更新了我的问题,并且.51
和.5
之间没有区别。但是,在6核沙桥上(你的也有6核)我在你的绘图中看到了类似的行为。每个divpd
操作有额外的2movapd
、1add
、1cmp
和jb
操作,但它们能解释agner表的巨大差异吗?这是我的第一个想法。发现不错。
import numpy as np
import timeit
import matplotlib.pyplot as plt
CPUHz = 3.3e9
divpd_cycles = 4.5
L2cachesize = 2*2**20
L3cachesize = 8*2**20
def timeit_command(command, pieces, size):
return min(timeit.repeat("for i in xrange(%d): %s" % (pieces, command),
"import numpy; A = numpy.random.rand(%d)" % size, number = 6))
def run():
totaliterations = 1e7
commands=["A/=0.5", "A/=0.51", "A/0.5", "A*=2.0", "A*2.0", "A+=2.0"]
styles=['-', '-', '--', '-', '--', '-']
def draw_graph(command, style, compute_overhead = False):
sizes = []
y = []
for pieces in np.logspace(0, 5, 11):
size = int(totaliterations / pieces)
sizes.append(size * 8) # 8 bytes per double
time = timeit_command(command, pieces, (4 if compute_overhead else size))
# Divide by 2 because SSE instructions process two doubles each
cycles = time * CPUHz / (size * pieces / 2)
y.append(cycles)
if compute_overhead:
command = "numpy overhead"
plt.semilogx(sizes, y, style, label = command, linewidth = 2, basex = 10)
plt.figure()
for command, style in zip(commands, styles):
print command
draw_graph(command, style)
# Plot overhead
draw_graph("A+=1.0", '-', compute_overhead=True)
plt.legend(loc = 'best', prop = {'size':9}, handlelength = 3)
plt.xlabel('Array size in bytes')
plt.ylabel('CPU cycles per SSE instruction')
# Draw vertical and horizontal lines
ymin, ymax = plt.ylim()
plt.vlines(L2cachesize, ymin, ymax, color = 'orange', linewidth = 2)
plt.vlines(L3cachesize, ymin, ymax, color = 'red', linewidth = 2)
xmin, xmax = plt.xlim()
plt.hlines(divpd_cycles, xmin, xmax, color = 'blue', linewidth = 2)