Numpy/Python 中基本数学运算的速度:为啥整数除法最慢?
Posted
技术标签:
【中文标题】Numpy/Python 中基本数学运算的速度:为啥整数除法最慢?【英文标题】:speed of elementary mathematical operations in Numpy/Python: why is integer division slowest?Numpy/Python 中基本数学运算的速度:为什么整数除法最慢? 【发布时间】:2019-08-02 11:15:45 【问题描述】: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。
似乎在 PythonNumpy 中,整数除法更多比常规除法(整数)昂贵,这是违反直觉的。测试时,我得到了这个:
setup_string = 'import numpy as np;\
N=int(1e5);\
x=np.arange(1,N+1, dtype=int);\
y=np.arange(N, dtype=int);'
加法 (+) ~ 0.1s
timeit("x+y", setup=setup_string, number=int(1e3))
0.09872294100932777
减法 (-) ~ 0.1s
timeit("x-y", setup=setup_string, number=int(1e3))
0.09425603999989107
乘法 (*) ~ 0.1s
timeit("x*y", setup=setup_string, number=int(1e3))
0.09888673899695277
除法 (/) ~ 0.35s
timeit("x/y", setup=setup_string, number=int(1e3))
0.3574664070038125
整数除法 (//) ~ 1s (!)
timeit("x//y", setup=setup_string, number=int(1e3))
1.006298642983893
任何想法为什么会发生这种情况?为什么整数除法不快?
【问题讨论】:
这不是关于 Python 的 整数除法,而是关于numpy
的。一种重要的区别。
你能指定你使用的是哪个版本的Python和numpy
吗?
Python 3.5.5, Numpy 1.15.0 另外,@ShadowRanger 你可能会在这里找到一些东西。在我的模拟中,我使用列表推导(因为我需要舍入整数除法)然后转换回 numpy 数组,所以 Python 和整数除法的 Numpy 实现之间可能存在(很大)差异。我会尽快测试。
@ShadowRanger 即使在 Python 中执行此操作,整数除法似乎也不会更快(请参阅更新的问题)
另外,setup_string
不是一个有效的字符串。
【参考方案1】:
简答:在硬件层面,浮点除法比整数除法更便宜。并且在至少一种通用架构上,浮点除法可以向量化,而整数除法不能,因此代码中最昂贵的运算必须执行更多次,整数运算的每次运算成本更高,而浮点运算执行的次数更少次,每次操作的成本更低。
长答案:numpy
在可用时使用矢量化数学,the x86-64 architecture (which I'm guessing you're using) doesn't provide a SIMD instruction for integer division。它只为整数提供向量化乘法(通过PMULUDQ
系列指令),但为浮点提供乘法(MULPD
family)和除法(DIVPD
family)。
当您使用/
进行真正的除法时,结果类型是float64
,而不是int64
,并且numpy
可以通过单个打包加载和转换来执行操作(使用the VCVTQQ2PD
family of operations,后跟一个打包除法,然后打包回内存 (MOVAPD
family)。
在采用 AVX512 的最现代 x86-64 芯片(至强融核 x200+ 和 Skylake-X 及更高版本,后者自 2017 年底起可用于桌面市场)上,每条此类矢量化指令可以同时执行八项操作(旧架构从 2011 年后可以用 AVX 做四个,在此之前你可以用 SSE2 做两个)。对于/
,这意味着您只需为每八个分区发出两个VCVTQQ2PD
s(每个源阵列一个),一个VDIVPD
和一个VMOVAPD
(所有EVEX
前缀为512 位操作)到被执行。相比之下,//
要执行相同的八次除法,它需要从内存发出八个MOV
s(加载左侧数组操作数),八次CQO
s(将左侧数组操作数符号扩展至 128 位)值IDIV
需要),八个IDIV
s(至少为您从右侧数组加载)和八个MOV
s 回到内存。
我不知道numpy
是否充分利用了这一点(我自己的副本显然是针对所有 x86-64 机器提供的 SSE2 基线编译的,所以它一次只进行两个分区,而不是八个分区),但它是可能,但根本无法对等价的整数运算进行矢量化。
虽然整数情况下的单独指令通常便宜一些,但它们基本上总是比组合的等效指令更昂贵。而对于整数除法,单个操作实际上比压缩操作的浮点除法更糟糕;每Agner Fog's Skylake-X table,每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 的情况下编译的,因此您从真正的除法中获得了更多的性能。
至于为什么非numpy
Pythonint
地板除法比真除法时间长,原因类似,但有一些复杂因素:
-
(帮助
int / int
,伤害int // int
)同样适用于numpy
的硬件指令开销问题; IDIV
比 FDIV
慢。
(隐藏性能差异)执行单个除法的解释器开销占总时间的更大百分比(减少相对性能差异,因为更多时间花在开销上)
(通常会增加整数运算的成本)在 Python int
s 上,整数除法的成本甚至更多;在 CPython 上,int
实现为 15 位或 30 位肢体数组,ssize_t
定义了签名和肢体数量。 CPython 的 float
只是普通 C double
的普通对象包装器,没有特殊功能。
(增加int // int
的成本)Python保证取整除法,但C只提供截断整数除法,所以即使在小int
s的快速路径中,Python也必须检查不匹配的符号并将操作调整为确保结果是地板,而不是简单的截断。
(增加int / int
运算的成本,专门针对大输入)CPython 的int / int
运算不仅将两个操作数都转换为float
(C double
)并执行浮点除法。当操作数足够小时,它会尝试这样做,但如果它们太大,它会使用复杂的后备算法来实现最佳的正确性。
(如果结果在短时间内被丢弃,则减少重复int / int
的成本)由于 Python float
s 是固定大小的,因此他们实现了较小的性能优化以使用空闲列表;当您反复创建和删除float
s 时,新创建的不需要进入内存分配器,它们只需从空闲列表中拉出,当引用计数降至零时释放到空闲列表。 CPython 的int
s 是可变长度的,不使用空闲列表。
总的来说,这一切都略微支持int / int
(至少对于小型int
s;大型int
情况变得更加复杂,但是对于int // int
来说可能也更糟,因为数组基于-的除法算法非常复杂/昂贵),因此使用 Python 内置类型看到类似的行为并不意外。
【讨论】:
以上是关于Numpy/Python 中基本数学运算的速度:为啥整数除法最慢?的主要内容,如果未能解决你的问题,请参考以下文章