我有一个情况,我需要用同一个数组划分几个不同的大数组:
import numpy as np
a = np.random.uniform(-1, 1, size=(1000000,))
b = np.random.uniform(-1, 1, size=(1000000,))
c = np.random.uniform(-1, 1, size=(1000000,))
d = np.random.uniform(1, 100, size=(1000000,))
a1 = a / d
b1 = b / d
c1 = c / d
我知道除法比乘法慢,所以我很好奇,如果我写:
d_recip = 1 / d
a1 = a * d_recip
b1 = b * d_recip
c1 = c * d_recip
numpy是否已经在引擎盖下做了一些事情来优化这样的情况?如果我使用cython,像这样的东西会被优化吗?
我也很好奇,当除数是标量时,这是如何应用的:
import numpy as np
a = np.random.uniform(-1, 1, size=(1000000,))
b = np.random.uniform(-1, 1, size=(1000000,))
c = np.random.uniform(-1, 1, size=(1000000,))
d = 1.65
a1 = a / d
b1 = b / d
c1 = c / d
# faster? or has numpy already optimized this above?
d_recip = 1 / d
a2 = a * d_recip
b2 = b * d_recip
c2 = c * d_recip
我知道倒数方法不会产生位相同的结果,我只是好奇如何获得最快的速度时,确切的位不是一个问题。
1条答案
按热度按时间nue99wik1#
Numpy在引擎盖下已经做了什么来优化这样的情况吗?
不。Numpy不会对这样的除法执行特殊优化(请参阅下面的证明)。
我知道除法比乘法慢
这确实是普遍正确的。几乎所有的平台。话虽如此,在最近的CPU上,实际上差距并不大,特别是当Numpy数组很大时。
实际上,* 整数 * 除法比乘法(特别是64位除法)昂贵得多。例如,在Intel Skylake(4-8岁)上,每21~90个周期可以计算一次64位整数除法,而每1个周期可以计算一次64位整数乘法。对于 * 浮点 * 数(FP),每4个周期可以计算 * 双精度 (64位)除法,而每1个周期可以计算2个双精度(DP)乘法。正如我们所看到的,浮点数的差距要小得多,尽管它仍然很大。不同CPU架构的数字会有所不同,但所有主流x86-64 CPU(Intel和AMD)的行为方式都是一样的:分割总是较慢(延迟和吞吐量)。尽管如此,在最近的CPU上,整数除法要快得多(而浮点数的情况大致相同)。事实上,64位整数除法在AMD Zen 4上只需要7-12个周期,在Intel IceLake上只需要10个周期,而64位整数乘法仍然需要1个周期。
在实践中,Numpy使用一个SIMD友好的代码,使C编译器能够生成快速的 SIMD 指令(例如:AVX),以便在运行时计算每个指令的多个数字。在这种情况下,乘法和除法之间的差距往往是两倍大,但两者都可以很快计算(对于Python代码)。例如,AVX可以每8个周期计算4次除法(即,0.5/循环)!
我很好奇,如果我写的更快:[...]
前一段只在计算是 * 计算绑定 * 的情况下才有意义。问题是计算在大多数机器上是内存受限的(或接近)。因此,除法的时间几乎是无关紧要的,因为RAM比CPU慢得多。话虽如此,结果可能会从一台机器到另一台机器发生变化,因为输入/输出阵列可以适应最近高端CPU的L3缓存,并且L3缓存比RAM快。此外,最近的高端机器有2个DDR5/LPDDR 5 RAM通道,可以达到相当高的带宽。Numpy可能无法使此类机器上的RAM饱和。在这种情况下,除法在这样的机器上可能会慢得多。
此外,由于Numpy * 分配内存 * 的方式,操作系统管理 * 虚拟内存页面 * 以及CPU缓存的操作方式(更不用说频率缩放管理),计时可能非常不稳定。因此,在编写基准时需要小心。最重要的是,计算
d_recip
会导致从RAM读取/写入更多的数据,如果合适的话,缓存中需要更多的空间(因此不太可能影响基准测试)。这也是为什么有些人觉得乘法比较慢的部分原因。在我的机器上,40 GiB/s RAM和4.5 GHz i5- 9600 KF CPU,可以计算0.5 DP分频/周期。这意味着如果RAM不是瓶颈,CPU可以达到
3 * 0.5 * 8 * 4.5e9 / 1024**3 = 50.3 GiB/s
的吞吐量。实际上,我的RAM的吞吐量较慢,因此乘法和除法应该同样快。添加一个新的临时步骤,对饱和的RAM施加更大的压力,只会使事情变慢(下面提供了一些分析细节)。如果我使用cython,像这样的东西会被优化吗?
嗯,这取决于很多参数...但总的来说不多。
在Cython中使用相同的代码不会对性能产生影响,因为Cython不会加速Numpy调用。如果你使用朴素的基本循环,那么Cython代码的行为将像Numpy一样。如果你在运行中计算
1 / d
的项,那么它将减少内存压力,所以第二个代码在所有主流机器上不应该比第一个慢。尽管如此,这两种代码的速度应该受到RAM的限制,所以应该关注这一点,而不是分割的成本。 * 更好的是:在运行中计算数据,以避免在RAM中写入巨大的数组。如果可能的话,阵列应该适合该高速缓存。在引擎盖下
下面是我在Debian Linux(带GCC)上使用numpy-1.26.1和CPython 3.11.2在我的机器上获得的一些分析细节。
以下是时间:
因此,可以看出,在这种情况下,乘法和除法之间的结果与上面所预期和解释的相同。
执行Numpy数组DP除法会导致调用名为
DOUBLE_divide_FMA3__AVX2
的计算函数,而DP乘法则是DOUBLE_multiply_FMA3__AVX2
。这两个函数的汇编代码如下:可以看到Numpy按照预期使用除法,并且除了使用AVX进行矢量化之外,没有执行任何特殊的优化。
更高级的分析结果表明,几乎所有的时间都花在加载/存储指令上,特别是在执行乘法时:乘法指令仅停止5%的时间,而除法指令仅停止32%的时间。在这两种情况下,我的RAM几乎都饱和了(阅读:23 GiB/s,写入:9.4 GiB/s)。请注意,每个核心的吞吐量是有限的,因此我希望在>= 2个核心的情况下并行达到~40 GiB/s。
备注
1请注意,因子3来自两点: