Python在类实例与局部(numpy)变量上的性能

Python在类实例与局部(numpy)变量上的性能,python,performance,numpy,class,matrix-multiplication,Python,Performance,Numpy,Class,Matrix Multiplication,我还读过另一篇关于python的速度/性能应该如何相对不受正在运行的代码是在main中、在函数中还是定义为类属性的影响的文章,但这些并没有解释我在使用类和局部变量时,特别是在使用numpy库时,在性能上看到的巨大差异。为了更清楚,我在下面做了一个脚本示例 import numpy as np import copy class Test: def __init__(self, n, m): self.X = np.random.rand(n,n,m)

我还读过另一篇关于python的速度/性能应该如何相对不受正在运行的代码是在main中、在函数中还是定义为类属性的影响的文章,但这些并没有解释我在使用类和局部变量时,特别是在使用numpy库时,在性能上看到的巨大差异。为了更清楚,我在下面做了一个脚本示例

import numpy as np
import copy 

class Test:
    def __init__(self, n, m):
        self.X = np.random.rand(n,n,m)
        self.Y = np.random.rand(n,n,m)
        self.Z = np.random.rand(n,n,m)
    def matmul1(self):
        self.A = np.zeros(self.X.shape)
        for i in range(self.X.shape[2]):
            self.A[:,:,i] = self.X[:,:,i] @ self.Y[:,:,i] @ self.Z[:,:,i]
        return
    def matmul2(self):
        self.A = np.zeros(self.X.shape)
        for i in range(self.X.shape[2]):
            x = copy.deepcopy(self.X[:,:,i])
            y = copy.deepcopy(self.Y[:,:,i])
            z = copy.deepcopy(self.Z[:,:,i])
            self.A[:,:,i] = x @ y @ z
        return

t1 = Test(300,100) 
%%timeit   
t1.matmul1()
#OUTPUT: 20.9 s ± 1.37 s per loop (mean ± std. dev. of 7 runs, 1 loop each)

%%timeit
t1.matmul2()
#OUTPUT: 516 ms ± 6.49 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
在这个脚本中,我将属性X、Y和Z定义为3向数组的类。我还有两个函数属性(matmul1和matmul2),它们循环遍历数组的第3个索引,并对3个切片中的每个切片进行矩阵乘法以填充数组,A.matmul1仅循环遍历类变量和矩阵乘法,而matmul2为循环中的每个矩阵乘法创建本地副本。Matmul1比matmul2慢约40倍。有人能解释为什么会这样吗?也许我在考虑如何不正确地使用类,但我也不会假设变量应该一直被深度复制。基本上,深度复制对我的性能有如此显著的影响,这在使用类属性/变量时是不可避免的吗?这似乎不仅仅是前面讨论的调用类属性的开销。欢迎您的任何意见,谢谢


编辑:我真正的问题是,为什么类实例变量的子数组的视图的副本,而不是它们的视图,会使这些类型的方法具有更好的性能。

如果您将
m
维度放在第一位,您就可以不经过迭代就完成此产品:

In [146]: X1,Y1,Z1 = X.transpose(2,0,1), Y.transpose(2,0,1), Z.transpose(2,0,1)
In [147]: A1 = X1@Y1@Z1
In [148]: np.allclose(A, A1.transpose(1,2,0))
Out[148]: True
但是,有时,由于内存管理的复杂性,使用非常大的阵列速度较慢

这可能值得一试

 A1[i] = X1[i] @ Y1[i] @ Z1[i]
其中迭代位于最外层维度上

我的计算机太小,无法对这些阵列大小进行良好计时

编辑 我将这些备选方案添加到您的类中,并用一个较小的案例进行了测试:

In [67]: class Test:
    ...:     def __init__(self, n, m):
    ...:         self.X = np.random.rand(n,n,m)
    ...:         self.Y = np.random.rand(n,n,m)
    ...:         self.Z = np.random.rand(n,n,m)
    ...:     def matmul1(self):
    ...:         A = np.zeros(self.X.shape)
    ...:         for i in range(self.X.shape[2]):
    ...:             A[:,:,i] = self.X[:,:,i] @ self.Y[:,:,i] @ self.Z[:,:,i]
    ...:         return A
    ...:     def matmul2(self):
    ...:         A = np.zeros(self.X.shape)
    ...:         for i in range(self.X.shape[2]):
    ...:             x = self.X[:,:,i].copy()
    ...:             y = self.Y[:,:,i].copy()
    ...:             z = self.Z[:,:,i].copy()
    ...:             A[:,:,i] = x @ y @ z
    ...:         return A
    ...:     def matmul3(self):
    ...:         x = self.X.transpose(2,0,1).copy()
    ...:         y = self.Y.transpose(2,0,1).copy()
    ...:         z = self.Z.transpose(2,0,1).copy()
    ...:         return (x@y@z).transpose(1,2,0)
    ...:     def matmul4(self):
    ...:         x = self.X.transpose(2,0,1).copy()
    ...:         y = self.Y.transpose(2,0,1).copy()
    ...:         z = self.Z.transpose(2,0,1).copy()
    ...:         A = np.zeros(x.shape)
    ...:         for i in range(x.shape[0]):
    ...:             A[i] = x[i]@y[i]@z[i]
    ...:         return A.transpose(1,2,0)

In [68]: t1=Test(100,50)
In [69]: np.max(np.abs(t1.matmul2()-t1.matmul4()))
Out[69]: 0.0
In [70]: np.allclose(t1.matmul3(),t1.matmul2())
Out[70]: True
视图
迭代速度慢10倍:

In [71]: timeit t1.matmul1()
252 ms ± 424 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [72]: timeit t1.matmul2()
26 ms ± 475 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
新增内容大致相同:

In [73]: timeit t1.matmul3()
30.8 ms ± 4.33 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [74]: timeit t1.matmul4()
27.3 ms ± 172 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
如果没有
copy()
,则
转置将生成一个视图,并且时间与
matmul1
(250ms)相似

我的猜测是,使用“新”副本,
matmul
能够通过引用将它们传递给最佳的BLAS函数。对于视图,如在
matmul1
中,它必须采取某种较慢的路线

但是如果我使用
dot
而不是
matmul
,即使使用
matmul1
iteation,我也能获得更快的时间

In [77]: %%timeit
    ...: A = np.zeros(X.shape)
    ...: for i in range(X.shape[2]):
    ...:     A[:,:,i] = X[:,:,i].dot(Y[:,:,i]).dot(Z[:,:,i])
25.2 ms ± 250 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

它看起来确实像是
matmul
with view正在进行一些次优的计算选择。

如果我的解释正确,你会想知道为什么deep-copy版本要快得多(上面的注释遗漏了什么?)。这需要分析和更多的分析步骤。但有两个明显的候选者:A:matmul2允许numpy委托给BLAS,而在matmul1中,它以某种方式失败了。大概也许不是。B:索引深度拷贝O(n^2)在执行某些O(n^3)操作之前确实会更改存储顺序。由于缓存阻塞(现代CPU),这有时会更快。这两种猜测都可能是错误的。这不是你确定的假设。分析将显示时间花在代数上。@juanpa.arrivillaga混淆的是,执行此昂贵的操作似乎会导致整体操作速度更快。
x=self.x[:,:,i].copy()
就足够了
deepcopy
仅当数组
dtype
为“对象”时才起作用。这并不影响您的计时。我认为这与“self.X[:,:,I]”不返回self.X中这些项目的副本有关,而是返回一个“视图对象”。然后在两个视图对象上执行matmul。我不知道为什么总的来说这会比matmul慢很多。也许这是个bug?
np.dot
即使没有拷贝,速度也与您的
matmul2
相似。看起来
matmul
采用了一些次优路线,数组是视图,无论是通过索引还是转置生成的。正如您在上一个代码块中所示,使用带有numpy.dot()的视图与@运算符的视图将导致我们所讨论的性能差异。我想你基本上回答了这个问题。如果我真的想深入了解这一点(例如,我需要避免由于数组大小而产生副本的函数,如果numpy.dot()就是这样的话),我会问一个更具体的问题。非常感谢。