Numpy / Python中基本数学运算的速度:为什么整数除法最慢?

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)

任何想法为什么会这样?为什么整数除法不快?

Sha*_*ger 8

简短答案:浮点除法在硬件级别比整数除法便宜。并且在至少一种通用体系结构上,可以对浮点除法进行矢量化处理,而不能对整数除法进行矢量化处理,因此,代码中最昂贵的操作必须执行更多次,并且整数运算的每次操作成本更高,而浮点数学则要少一些次,每次操作成本更低。

长答案:numpy使用向量化数学(如果可用),并且x86-64体系结构(我猜您正在使用)不提供用于整数除法的SIMD指令。它仅提供整数的矢量化乘法(通过PMULUDQ指令族),但同时提供浮点的乘法(MULPDfamily)和除法(DIVPDfamily)。

当你使用/真正的鸿沟,结果类型float64,不int64numpy可以通过一个单一的打包负荷和转换(与执行操作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底数划分需要比真正的划分更长的时间,这是类似的原因,但是有一些复杂的因素:

  1. (帮助int / int,伤害int // int)相同的硬件指令开销问题来自于numpy应用;IDIV比慢FDIV
  2. (隐藏性能差异)执行单个划分的解释器开销占总时间的百分比更大(减少相对性能差异,因为更多的时间花费在开销上)
  3. (通常会增加整数运算的成本)在Python上,整数除法甚至更加昂贵int;在CPython上,int它以15位或30位肢体的数组形式实现,其中ssize_t定义了肢体的签名和数量。CPython float只是普通C周围的普通对象包装double,没有特殊功能。
  4. (增加了成本int // int)Python保证底数划分,但是C只提供截断整数除法,因此即使在小ints 的快速路径中,Python也必须检查不匹配的符号并调整操作以确保结果是底数,而不是简单截断。
  5. int / int专门为大型输入增加了操作成本)CPython的int / int操作不仅将两个操作数都转换为float(C double)并执行浮点除法。当操作数足够小时,它会尝试这样做,但是如果操作数太大,它会使用复杂的后备算法来实现最佳的正确性。
  6. int / int如果结果是在短时间内被丢弃,则可以减少重复的开销)由于Python float是固定大小的,因此他们实现了较小的性能优化,以便为其使用一个空闲列表。当您float重复创建和删除s时,新创建的s不需要进入内存分配器,它们只是从空闲列表中拉出,并在引用计数降至零时释放到空闲列表。CPython的int长度是可变的,并且不使用空闲列表。

总的来说,这都是有点支持的int / int(至少对于小ints而言;大的int情况会变得更复杂,但是随后int // int,由于基于数组的除法算法极其复杂/昂贵,这也可能会变得更糟),因此看到了类似的行为使用Python内置类型并不奇怪。