PDi*_*lta 5 python performance numpy integer-division integer-arithmetic
EDIT2:正如@ShadowRanger指出的那样,这是一种Numpy现象,而不是Python。但是,当在Python中使用列表推导进行计算(因此x+y变为[a+b for a,b in zip(x,y)])时,所有算术运算仍会花费同样长的时间(尽管是Numpy的100倍以上)。但是,当我在真实的仿真中使用整数除法时,它们的运行速度会更快。因此,主要问题仍然存在:即使在Python中,为什么这些测试表明整数除法没有比常规除法更快?
EDIT1:版本:Python 3.5.5,Numpy 1.15.0。
似乎在Python Numpy中,整数除法比(整数的)正规除法更昂贵,这是违反直觉的。测试时,我得到以下信息:
setup_string = 'import numpy as np;\
N=int(1e5);\
x=np.arange(1,N+1, dtype=int);\
y=np.arange(N, dtype=int);'
Run Code Online (Sandbox Code Playgroud)
加法(+)〜0.1s
timeit("x+y", setup=setup_string, number=int(1e3))
0.09872294100932777
Run Code Online (Sandbox Code Playgroud)
减法(-)〜0.1s
timeit("x-y", setup=setup_string, number=int(1e3))
0.09425603999989107
Run Code Online (Sandbox Code Playgroud)
乘法(*)〜0.1s
timeit("x*y", setup=setup_string, number=int(1e3))
0.09888673899695277
Run Code Online (Sandbox Code Playgroud)
除(/)〜0.35s
timeit("x/y", setup=setup_string, number=int(1e3))
0.3574664070038125
Run Code Online (Sandbox Code Playgroud)
整数除(//)〜1s(!)
timeit("x//y", setup=setup_string, number=int(1e3))
1.006298642983893
Run Code Online (Sandbox Code Playgroud)
任何想法为什么会这样?为什么整数除法不快?
简短答案:浮点除法在硬件级别比整数除法便宜。并且在至少一种通用体系结构上,可以对浮点除法进行矢量化处理,而不能对整数除法进行矢量化处理,因此,代码中最昂贵的操作必须执行更多次,并且整数运算的每次操作成本更高,而浮点数学则要少一些次,每次操作成本更低。
长答案:numpy使用向量化数学(如果可用),并且x86-64体系结构(我猜您正在使用)不提供用于整数除法的SIMD指令。它仅提供整数的矢量化乘法(通过PMULUDQ指令族),但同时提供浮点的乘法(MULPDfamily)和除法(DIVPDfamily)。
当你使用/真正的鸿沟,结果类型float64,不int64和numpy可以通过一个单一的打包负荷和转换(与执行操作的VCVTQQ2PD家庭作业,随后打包分,随后打包搬回内存(MOVAPD系列) 。
在配备AVX512的最现代的x86-64芯片(至强融核x200 +和Skylake-X及更高版本,后者自2017年末开始在台式机市场上提供)上,每个这样的矢量化指令都可以一次执行八次操作(从2011年可以使用AVX进行四项操作,而在此之前,您可以使用SSE2进行两项操作。对于/,这意味着您只需要每执行八次除法就发出两个VCVTQQ2PDs(每个源阵列一个),一个VDIVPD和一个VMOVAPD(所有EVEX前缀为512位操作)。相比之下,//要执行相同的八次除法,则需要MOV从内存中发出八秒(以加载左数组操作数),八CQO秒(以符号方式将左数组操作数扩展到IDIV需要的128位值),八IDIVs(至少从右侧数组加载),然后再8 MOVs返回内存。
我不知道是否numpy充分利用了这一点(我自己的副本显然是针对所有x86-64机器提供的SSE2基准进行编译的,因此它一次只能进行两个分区,而不是八个分区),但是有可能在没有分区的情况下向量化等效整数运算的方法。
尽管整数情况下的单个指令通常要便宜一些,但它们基本上总是比组合等效指令贵。对于整数除法,单操作实际上比打包操作的浮点除法更糟。根据Agner Fog的Skylake-X表格,每笔成本IDIV为24-90个周期,延迟为42-95;VDIVPD所有512位寄存器的成本为16个周期,延迟为24个周期。VDIVPD不仅完成了八倍的工作,而且(最多)完成了三分之二的周期IDIV(我不知道为什么IDIV周期数范围如此之大,但VDIVPD即使是IDIV)。对于普通的AVX操作(每个操作仅进行四个除法VDIVPD),每个操作的周期减少一半(减少为八个),而DIVPD每个指令的普通两个除法操作的周期仅为四个周期,因此无论您是否使用,除法本身的速度基本相同SSE2,AVX或AVX512指令(AVX512只是为您节省了一点延迟,以及加载/存储开销)。即使从未使用过矢量化指令,普通指令也FDIV只是一个4-5周期的指令(二进制浮点除法通常比整数除法更容易,请参见图),因此您希望看到浮点数学的效果很好。
Point在硬件级别上,划分许多64位浮点值要比划分许多64位整数值便宜,因此使用进行真正的除法/本质上比使用进行地板除法要快//。
在我自己的机器上(我已经验证了它仅使用基线SSE2 DIVPD,因此每条指令只进行两次除法),我尝试复制您的结果,并且时间上的差异较小。真正的划分,每个操作花费485?s,而楼层划分,每个操作花费1.05 ms;楼层划分仅略多于2倍,而对您而言,则几乎快3倍。猜想,您的副本numpy是在支持AVX或AVX512 的情况下编译的,结果您从真正的划分中挤出了更多性能。
至于为什么非numpyPython int底数划分需要比真正的划分更长的时间,这是类似的原因,但是有一些复杂的因素:
int / int,伤害int // int)相同的硬件指令开销问题来自于numpy应用;IDIV比慢FDIV。int;在CPython上,int它以15位或30位肢体的数组形式实现,其中ssize_t定义了肢体的签名和数量。CPython float只是普通C周围的普通对象包装double,没有特殊功能。int // int)Python保证底数划分,但是C只提供截断整数除法,因此即使在小ints 的快速路径中,Python也必须检查不匹配的符号并调整操作以确保结果是底数,而不是简单截断。int / int专门为大型输入增加了操作成本)CPython的int / int操作不仅将两个操作数都转换为float(C double)并执行浮点除法。当操作数足够小时,它会尝试这样做,但是如果操作数太大,它会使用复杂的后备算法来实现最佳的正确性。int / int如果结果是在短时间内被丢弃,则可以减少重复的开销)由于Python float是固定大小的,因此他们实现了较小的性能优化,以便为其使用一个空闲列表。当您float重复创建和删除s时,新创建的s不需要进入内存分配器,它们只是从空闲列表中拉出,并在引用计数降至零时释放到空闲列表。CPython的int长度是可变的,并且不使用空闲列表。总的来说,这都是有点支持的int / int(至少对于小ints而言;大的int情况会变得更复杂,但是随后int // int,由于基于数组的除法算法极其复杂/昂贵,这也可能会变得更糟),因此看到了类似的行为使用Python内置类型并不奇怪。