【问题标题】:Python performace on class instance vs local (numpy) variables类实例与本地(numpy)变量的 Python 性能
【发布时间】:2021-05-13 16:18:05
【问题描述】:

我已经阅读了其他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 属性的类定义为 3 路数组。我还有两个函数属性(matmul1 和 matmul2),它们循环遍历数组的第三个索引,矩阵乘以 3 个切片中的每一个来填充数组,A. matmul1 只是循环遍历类变量和矩阵乘法,而 matmul2 创建本地副本对于循环中的每个矩阵乘法。 Matmul1 比 matmul2 慢约 40 倍。有人可以解释为什么会这样吗?也许我正在考虑如何错误地使用类,但我也不认为变量应该一直被深度复制。基本上,深度复制对我的性能影响如此之大的原因是什么,并且在使用类属性/变量时这是不可避免的吗?它似乎不仅仅是调用类属性的开销here。任何意见表示赞赏,谢谢!

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

【问题讨论】:

  • 如果我解释正确,你想知道为什么深拷贝版本要快得多(上面的评论遗漏了什么?)。这需要分析和更多的分析步骤。但是有两个明显的候选者: A:matmul2 indexed 允许 numpy 委托给 BLAS,而在 matmul1 中它以某种方式失败了。可能是?也许不吧。 B:索引深拷贝 O(n^2) 在执行一些 O(n^3) 操作之前确实会更改 存储顺序。由于缓存阻塞(现代 CPU),这有时会更快。两者都是可能是错误的猜测。这不是你肯定的假设。分析将显示时间花在代数上。
  • @juanpa.arrivillaga 令人困惑的是,执行这种昂贵的操作似乎会导致整体操作更快
  • x=self.X[:,:,i].copy() 就足够了。 deepcopy 仅在数组 dtype 是 `object.并不是说这会影响您的时间安排。
  • 我认为这与“self.X[:,:,i]”不返回self.X中这些项目的副本有关,它返回一个“视图目的”。然后,您正在对两个视图对象执行 matmul。我不知道为什么这通常会比 matmul 慢得多。也许这是一个错误?
  • np.dot 的速度与您的 matmul2 相似,即使没有副本也是如此。看起来matmul 正在采取一些次优路线,数组是视图,无论是通过索引还是转置生成。

标签: python performance numpy class matrix-multiplication


【解决方案1】:

如果你把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 会产生一个视图,时间与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 with views 确实采取了一些次优的计算选择。

【讨论】:

  • 正如您在上一个代码块中所展示的,使用带有 numpy.dot() 的视图与 @ 运算符将导致我们一直在讨论的性能差异。我想你基本上已经回答了这个问题。如果我想真正深入了解这一点(例如,我需要避免由于数组大小而进行复制的函数,如果这甚至是 numpy.dot() 的情况),我会问一个更具体的问题。谢谢!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-02-12
  • 2014-09-21
  • 2013-03-22
  • 2016-06-26
  • 2013-03-24
  • 1970-01-01
相关资源
最近更新 更多