它和复数乘法是一样的,这让我很困惑:
import numpy as np
def op0(x):
return np.conj(x)
def op1(x0, x1):
return x0 * x1
def op2(x0, x1):
x0[:] = x1
for N in (50, 500, 5000):
print(f"\nshape = ({N}, {N})")
x0 = np.random.randn(N, N) + 1j*np.random.randn(N, N)
x1 = np.random.randn(N, N) + 1j*np.random.randn(N, N)
%timeit op0(x0)
%timeit op1(x0, x1)
%timeit op2(x0, x1)
shape = (50, 50)
3.55 µs ± 143 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
4.85 µs ± 261 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
1.85 µs ± 116 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
shape = (500, 500)
1.52 ms ± 60.6 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
1.96 ms ± 133 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
299 µs ± 50.2 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
shape = (5000, 5000)
163 ms ± 4.4 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
185 ms ± 11.5 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
39.8 ms ± 399 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
为什么翻转x.imag
的标志这么贵?当然,在低层次上,它比几个乘法和加法((a + j*b)*(c + j*d)
)容易得多?
Windows 10 x64、numpy 1.23.5、Python 3.10.4
3条答案
按热度按时间ndasle7k1#
在记忆中来回移动需要花费大部分时间。
我想出了类似于莎拉梅塞尔的想法(在上面的评论中)。仅复制输入数组就需要花费大量时间:
这些操作之间的时间差约为20%。
就地共轭
正如杰罗姆在评论中提到的,使用
out
参数:对于N=5000,在我的机器上快3倍。
对于小的N,改进是可忽略的。有趣的是,对于N=500,加速甚至更好(5-6x)。
iqjalb3h2#
测试更充分,完整的结果在底部。主要成果:
得分
NumPy中的
operation(x, out=x)
使其就位,如果错了请纠正我。将out=out
与预分配的out
一起使用会更慢(见下文)。所以...我想是被说服了不确定我是否应该对复数乘法的速度印象深刻,或者对基本的数组东西的速度感到失望。
操作分解
设
S = N**2
:op4
:S
读取,S
写入--S
符号翻转op5
:4*S
读取,2*S
写入--4*S
乘法,4*S
加法op6
:2*S
读取,2*S
写入op4
与op6
:S
读取,S
写入与S
符号翻转相当-似乎公平。op5
与op6
:额外的2*S
读取,4*S
乘法,4*S
加法甚至不会使计算时间加倍。写作是最贵的吗?如果我的分析出错了就告诉我。
完整结果
pw9qyyiw3#
以下是基于许多以前的评论的答案:
不确定我是否应该对复数乘法的速度印象深刻,或者对基本的数组东西的速度感到失望。
内存访问很慢,并且由于内存墙(开始于>20年前),将来会更慢。如果你想要快速的东西,你不需要使用你的DRAM。Numpy并不是专门为此设计的(这是相当可悲的)。您的硬件达到17.2 GiB/s。这不是很多,但也不是很糟糕。现代PC可以达到40-60 GiB/s(有些,如M1,甚至可以达到>60 GiB)。现代英特尔CPU L3缓存可以达到~400 GiB/s,所以更多。
GPU通常具有显著更高的内存吞吐量,但比率
computational_speed
/memory_bandwidth
仍然像CPU一样高(实际上甚至更高)。CPU现在有相当大的缓存,而GPU通常没有。请注意,GPU计算可能会被某些API延迟(惰性计算),因此您应该在基准测试中关注这一点(例如,您可以打印值)。op 5 vs op 6:额外的2S读取,4S乘法,4*S加法甚至不会使计算时间加倍。写作是最贵的吗?
这里所有的操作都应该是内存受限的。只有内存操作才重要。
op5
比op6
慢,因为op5
读取两个数组,而op6
读取一个。需要从DRAM传输更多数据,因此需要更多时间。此外,写可能比读更昂贵,但这取决于编译的汇编代码(因此编译器,优化标志和实际源代码)和目标体系结构。内存性能是一个复杂的主题,比看起来要复杂得多(有关此方面的更多信息,请参阅下文)。请注意,它是两个独立的数组并不重要。一个大阵列实际上分为两部分将具有相同的影响。与硬件没有太大区别。
一般注意事项
关于计时,现代内存/CPU非常复杂,因此通常不容易理解基于基准测试的情况(特别是没有汇编代码)。从DRAM的Angular 来看,写入和读取几乎同样快。由于DRAM的工作方式,混合它们会降低性能。现代x86-64 CPU缓存使用写分配策略,导致在执行写操作时执行读操作。这导致写入比读取慢2倍。也就是说,假设编译器生成了非时态指令,那么可以使用非时态指令来避免这个问题
编译器通常不会生成非临时指令,因为当数组适合该高速缓存时,它们可能会慢得多,在这种情况下,用于构建Numpy的编译器无法知道数组的大小(运行时定义)。它也无法知道CPU缓存的大小。内存复制倾向于使用
memcpy
基本函数,该函数经过优化,可针对目标平台的大型数组使用非临时指令。AFAIK,这样的指令在Numpy中不用于乘法/加法等操作。(至少在我的机器上不是)。请注意,我提到“现代CPU”是因为Zen之前的AMD CPU使用了一种非常不同的方法。在这种情况下,其他CPU体系结构(如POWER)的行为也非常不同。在高性能方面,任何细节都很重要。这也是为什么我说这个主题很复杂。最好的方法是在您的特定机器上进行低级分析,或者列出您的硬件,确切的Numpy包(或使用的汇编代码),以避免考虑理论上可能发生的许多可能的事情。