这个方面还有待改进。
操作不仅是在复制,而是因为它加载和存储到主内存而不是使用缓存。
通常情况下处理器会访问多个块的多个连续字节,然后从缓存中使用它。如果缓存空间不足,则会删除一些旧的块。
为了论证,假设您的CPU每个块包含8个字节,并且行是相邻的。在一个矩阵中,您将访问列,在另一个矩阵中您将访问行。当您写入一列时,您正在加载多个列但只更新一个。通过复制几列可以看出这个单列的开销。
n = 2**14
A = np.random.randint(0, 100, (n,n), dtype=np.int8)
B = np.empty_like(A)
%%timeit
B[:1,:] = A[:1,:]
%%timeit
B[:4,:] = A[:4,:]
如果你在行上做同样的操作,你应该会发现大致呈线性。如果你复制列,复制一列的成本非常接近于复制两列甚至8列或16列,这取决于硬件。
我将使用
n=2**14
来简化事情,但这个原则适用于任何维度。
- 如果你有足够小的矩阵,比如 8 x 8,整个矩阵都可以放入缓存中,因此你可以在不访问任何缓存的情况下转置它。
- 如果你正在复制大块连续的数据,即使你不能在缓存中完成整个操作,也可以减少给定数据从/到内存加载的次数。
基于这个,我尝试重新排列矩阵,将其变成较小的连续块的矩阵,首先我转置一个块中的元素,然后转置矩阵中的块。
对于基准测试:
B = np.ascontiguousarray(A.T)
3.12 s ± 446 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
使用8x8块
T0 = A.reshape(2048,8,2048,8)
T1 = np.ascontiguousarray(T0.transpose(0,2,3,1))
T2 = np.ascontiguousarray(T1.transpose(1,0,2,3))
T3 = np.ascontiguousarray(T2.transpose(0,2,1,3))
B = T3.reshape(A.shape)
786 ms ± 54.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
assert np.all(B == A.T)
它仍然比简单复制慢200倍,但已经比原始方法快4倍。
只分配两个临时数组而不是3个也有帮助。
T0 = np.empty_like(A)
T1 = np.empty_like(A)
T0.reshape(2048,2048,8,8)[:] = A.reshape(2048,8,2048,8).transpose(0,2,3,1)
T1.reshape(2048,2048,8,8)[:] = T0.reshape(2048,2048,8,8).transpose(1,0,2,3)
T0.reshape(2048,8,2048,8)[:] = T1.reshape(2048,2048,8,8).transpose(0,2,1,3)
B = T0
686 ms ± 60.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)