Python class 实例与局部(numpy)变量的性能

Python performace on class instance vs local (numpy) variables

我读过其他 posts 关于 python speed/performance 应该如何相对不受 运行 代码是否只是在 main、函数或定义中的影响作为 class 属性,但这些并不能解释我在使用 class 与局部变量时看到的非常大的性能差异,尤其是在使用 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)

在这个脚本中,我定义了一个 class 属性 X、Y 和 Z 作为三向数组。我还有两个函数属性(matmul1 和 matmul2),它们循环遍历数组的第三个索引和矩阵乘以 3 个切片中的每一个以填充数组,A. matmul1 只是循环遍历 class 变量和矩阵乘法,而matmul2 为循环内的每个矩阵乘法创建本地副本。 Matmul1 比 matmul2 慢 40 倍。有人可以解释为什么会这样吗?也许我正在考虑如何错误地使用 classes,但我也不认为变量应该一直被深度复制。基本上,深度复制对我的性能影响如此之大的原因是什么?使用 class attributes/variables 时这是不可避免的吗?它似乎不仅仅是 中讨论的调用 class 属性的开销。感谢任何输入,谢谢!

编辑: 我真正的问题是为什么 class 实例变量的子数组的副本而不是视图会导致这些类型的方法有更好的性能.

如果将 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]

迭代在最外层维度上。

我的电脑太小,无法对这些数组大小进行良好的计时。

编辑

我将这些备选方案添加到您的class,并使用较小的案例进行了测试:

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

view 迭代速度慢 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()transpose 会产生一个视图,时间与 matmul1(250 毫秒)相似。

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

但是如果我使用 dot 而不是 matmul,即使使用 matmul1 迭代,我的时间也会更快。

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 的视图确实采用了一些次优的计算选择。