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

标签 python performance numpy class matrix-multiplication

我读过其他posts关于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 作为三向数组。我还有两个函数属性(matmul1 和 matmul2),它们循环遍历数组的第三个索引并矩阵乘以 3 个切片中的每一个以填充数组,A. matmul1 仅循环遍历类变量和矩阵乘法,而 matmul2 创建本地副本对于循环内的每个矩阵乘法。 Matmul1 比 matmul2 慢约 40 倍。有人可以解释为什么会发生这种情况吗?也许我正在考虑如何错误地使用类,但我也不认为变量应该一直被深度复制。基本上,深度复制对我的性能影响如此之大,并且在使用类属性/变量时这是不可避免的吗?看起来它不仅仅是如所讨论的调用类属性的开销here 。欢迎任何意见,谢谢!

编辑:我真正的问题是为什么使用类实例变量的子数组的副本而不是 View 来为这些类型的方法带来更好的性能。

最佳答案

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

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 会生成一个 View ,并且时间与 matmul1 类似(250 毫秒)。

我的猜测是,通过“新鲜”副本,matmul 能够通过引用将它们传递给最佳的 BLAS 函数。对于 View ,如 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)

看起来带有 View 的 matmul 确实采取了一些次优的计算选择。

关于类实例与局部(numpy)变量的 Python 性能,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/66129074/

相关文章:

python - 如何加速 numpy 数组的枚举/如何有效地枚举 numpy 数组?

python - Numpy:更快地计算涉及求和的三重嵌套循环

Python Pandas 合并键错误

python - 新的 sympy 交叉点用法

python - 如何将父类方法的内容添加到子类方法中

python - 如何从 sklearn LinearRegression 导出线性回归公式

css - 颜色声明之间的性能差异?

mysql - 开发显示查询

当 null 存在时,SQL 查询返回 null 而不是 SUM

python - Scipy 稀疏 csr 矩阵在 0.0/1.0 上返回 nan