考虑以下Python代码,与乘以非转置矩阵相比,乘以预转置矩阵会产生更快的执行时间:
import numpy as np
import time
# Generate random matrix
matrix_size = 1000
matrix = np.random.rand(matrix_size, matrix_size)
# Transpose the matrix
transposed_matrix = np.transpose(matrix)
# Multiply non-transposed matrix
start = time.time()
result1 = np.matmul(matrix, matrix)
end = time.time()
execution_time1 = end - start
# Multiply pre-transposed matrix
start = time.time()
result2 = np.matmul(transposed_matrix, transposed_matrix)
end = time.time()
execution_time2 = end - start
print("Execution time (non-transposed):", execution_time1)
print("Execution time (pre-transposed):", execution_time2)
字符串
令人惊讶的是,乘以预转置矩阵更快。人们可能会认为乘法的顺序不应该显著影响性能,但似乎存在差异。
为什么处理预转置矩阵比处理非转置矩阵的执行时间更快?是否有任何潜在的原因或优化来解释这种行为?
更新
我已经考虑了关于cache
的评论,并且在每个循环中生成新的矩阵:
import numpy as np
import time
import matplotlib.pyplot as plt
# Generate random matrices
matrix_size = 3000
# Variables to store execution times
execution_times1 = []
execution_times2 = []
# Perform matrix multiplication A @ B^T and measure execution time for 50 iterations
num_iterations = 50
for _ in range(num_iterations):
matrix_a = np.random.rand(matrix_size, matrix_size)
start = time.time()
result1 = np.matmul(matrix_a, matrix_a)
end = time.time()
execution_times1.append(end - start)
# Perform matrix multiplication A @ B and measure execution time for 50 iterations
for _ in range(num_iterations):
matrix_b = np.random.rand(matrix_size, matrix_size)
start = time.time()
result2 = np.matmul(matrix_b, matrix_b.T)
end = time.time()
execution_times2.append(end - start)
# Print average execution times
avg_execution_time1 = np.mean(execution_times1)
avg_execution_time2 = np.mean(execution_times2)
#print("Average execution time (A @ B^T):", avg_execution_time1)
#print("Average execution time (A @ B):", avg_execution_time2)
# Plot the execution times
plt.plot(range(num_iterations), execution_times1, label='A @ A')
plt.plot(range(num_iterations), execution_times2, label='B @ B.T')
plt.xlabel('Iteration')
plt.ylabel('Execution Time')
plt.title('Matrix Multiplication Execution Time Comparison')
plt.legend()
plt.show()
# Display BLAS configuration
np.show_config()
型
结果如下:
的数据
blas_mkl_info:
libraries = ['mkl_rt']
library_dirs = ['C:/Users/User/anaconda3\\Library\\lib']
define_macros = [('SCIPY_MKL_H', None), ('HAVE_CBLAS', None)]
include_dirs = ['C:/Users/User/anaconda3\\Library\\include']
blas_opt_info:
libraries = ['mkl_rt']
library_dirs = ['C:/Users/User/anaconda3\\Library\\lib']
define_macros = [('SCIPY_MKL_H', None), ('HAVE_CBLAS', None)]
include_dirs = ['C:/Users/User/anaconda3\\Library\\include']
lapack_mkl_info:
libraries = ['mkl_rt']
library_dirs = ['C:/Users/User/anaconda3\\Library\\lib']
define_macros = [('SCIPY_MKL_H', None), ('HAVE_CBLAS', None)]
include_dirs = ['C:/Users/User/anaconda3\\Library\\include']
lapack_opt_info:
libraries = ['mkl_rt']
library_dirs = ['C:/Users/User/anaconda3\\Library\\lib']
define_macros = [('SCIPY_MKL_H', None), ('HAVE_CBLAS', None)]
include_dirs = ['C:/Users/User/anaconda3\\Library\\include']
Supported SIMD extensions in this NumPy install:
baseline = SSE,SSE2,SSE3
found = SSSE3,SSE41,POPCNT,SSE42,AVX,F16C,FMA3,AVX2
not found = AVX512F,AVX512CD,AVX512_SKX,AVX512_CLX,AVX512_CNL
型
1条答案
按热度按时间j8ag8udp1#
在我的机器上看起来不太明显。
跑了1000次我得到这些时间(x=非转置,y=转置)。红点(在y=x轴下)比蓝点多。685/315更准确。因此,p值方面,毫无疑问,这不可能只是随机效应。(1000枚硬币,685个头像是一个明显的异常)
x1c 0d1x的数据
但时间上,这并不明显。集群主要集中在y=x轴上。
现在我开始回答这个问题,因为我很确定这是一个缓存问题。当我在工程学校的时候(很久以前,当这些考虑因素现在变得更加重要时,并且由教师教授,他们自己可以追溯到更重要的时候),在HPC课程中,我们被教导在从Fortran切换到C时要非常小心,因为缓存效应:当迭代一个数组时,按照它在内存中的顺序进行迭代是非常重要的(在numpy中仍然称为“C”顺序与“Fortran”顺序,证明对于那些比我更关心的人来说,这仍然是一个重要的考虑因素-我很少需要在日常工作中关心,因此我调用学校内存而不是作业内存)。
因为在处理内存中刚刚处理的数字旁边的数字时,该数字可能已经加载到缓存中。而如果您处理的下一个数字是1行之下(按C顺序,因此在内存中更远),那么它更有可能不在缓存中。以目前的高速缓存规模,需要很大的矩阵,这使得它的区别。
由于
transpose
不移动任何数据,而只是调整步长,因此处理转置矩阵的效果是改变了处理数据在内存中的顺序。所以如果你考虑一下简单的算法字符串
如果
A
和B
是C阶的,那么矩阵A的迭代是按照存储器的顺序进行的(我们沿着一行,一列接一列地迭代,所以存储器中的相邻数字一个接一个),而B
不是。如果该顺序是颠倒的,例如,因为它们已经被转置,那么它是相反的。B以不会造成缓存问题的顺序迭代,而A则不会。
好吧,没有必要在这上面停留太久,因为我告诉所有这些是为了解释为什么我想研究缓存问题的可能性(我的意图是将相同的乘法与转置矩阵的副本进行比较,以便它是相同的矩阵乘法,只有顺序改变。并且还尝试查看是否存在矩阵大小的阈值,在该阈值下该现象不可见,这也将验证高速缓存问题,因为对于这一点,整个矩阵必须不适合高速缓存)
但是,这样做的第一步也是开始避免偏差,因为第一计算使用尚未在高速缓存中的数据,而第二计算使用已经在高速缓存中的数据(特别是在整个矩阵适合高速缓存的情况下)。
这是我尝试的第一件事:只是颠倒了计算顺序。首先计算transposed_matrix,然后计算matrix。
的
这一次,移位有利于蓝点(当然,我只改变了计算顺序,没有改变轴的含义。所以x仍然是
matrix@matrix
时序,y仍然是transposed_matrix
这次红点的数量是318对682。所以,几乎和以前完全相反。
所以,结论(至少对我的机器有效):这确实是高速缓存问题。但是,缓存问题仅由以下事实引起,即存在有利于
transposed_matrix
的偏见:当你用它来计算时,它已经在缓存中了(因为数据和矩阵的数据是一样的)。编辑:关于问题更新。
正如我在评论中所说的(但由于这个问题已经更新了,这个答案,已经相当多了,我认为它也出现在这个答案中很重要,对于未来的读者来说,更新是不同的。
您的第一个问题是关于
A@A
与A.T@A.T
。第二个似乎更快。但这只是一次手术。所以,正如我所展示的,原因只是由于这样一个事实,即当第二个操作完成时,A
已经在缓存内存中(当第一个操作完成时,情况并非如此)。因为A.T
与A
是相同的数据(不是副本。但是相同的数据,在相同的存储器地址)。我之前的答案表明,如果你反过来,先计算A.T@A.T
,然后计算A@A
,那么它是,相反,A.T@A.T
更慢,并且比例完全相同。另一种表现方式是
型
(The事实上,在timeit之前执行
A@A
,只是为了确保20次计算中的第一次不会因为缓存考虑而变慢)在我的计算机上,所有这些操作几乎都需要1秒(选择
number=20
是为了花费1秒)这一次,没有缓存效应,因为我们每次运行21次,第一次运行时不计算时间。
.T
无影响现在,对于你的问题更新,这是另一回事
型
这一次,前两个操作仅需要650 ms。而不是因为缓存:无论这些操作的顺序如何都是相同的。
这是因为numpy能够检测到
A.T
和A
是相同的矩阵,但只有一次转置操作。(这是很容易发现的:它是相同的数据地址,但是步幅和形状(这里的形状是正方形;但更重要的是,步幅是倒置的)是倒置的:A.strides
→(8000,8)
,A.T.strides
→(8, 8000)
。因此,numpy很容易意识到这是一个
A@A.T
的情况。因此,应用一种算法,计算速度更快。正如在评论中所说的(在我之前在评论中说过,还有其他人,几天前,谁误解了你的第一个问题......但这样做是正确的,因为他们提前回答了现在的更新):A@A.T
是对称的这里有一些简单的修正。注意
型
都是1秒(如
A@A
和A.T@A.T
)。因此,很容易理解A@B
,A@A
,A.T@A.T
都只使用一个标准的“矩阵乘法”算法。其中A@A.T
,A.T@A
使用更快的一个。由于
B
是A
的副本,因此A@B.T
与A@A.T
具有相同的对称结果。但这一次,因为是副本,numpy无法意识到它是一个A@A.T
的情况,无法意识到它是一个对称的结果(在计算结果之前)。因此A@B.T
与A@A
具有相同的标准“1秒”计时。而A@A.T
没有。这证实了它确实依赖于
same address, inverted strides
标准。只要不是相同的地址,或者不是相同的步幅倒置,标准算法,1秒。如果是两个地址相同,但步幅相反,那么,特殊算法,650毫秒。