多线程计算矩阵乘法性能测试
目录:
1.手工方法计算乘法矩阵
1.1任意的 m**n 阶矩阵A和nk阶矩阵B核心代码(附件1)
1.2任意的两个m阶矩阵乘法用于测试代码运行效率(附件2)
2.分治法计算乘法矩阵
2.1普通递归分治法计算矩阵乘法(附件3)
2.2Strasses法计算矩阵乘法(附件4)
3.多线程(进程)计算乘法矩阵
3.1利用python自带的threadingpool库函数的线程池方法(附件5)
3.2利用python的多进程multiprocessing库函数实现矩阵乘法计算(附件6)
3.2.1核心代码
3.2.2性能比较
3.3利用Java的多线程实现矩阵乘法的并行计算(附件7)
3.3.1核心代码
3.3.2性能比较
说明:
矩阵的乘法公式为C(m,k)=A(m,n)*B(n,k),当单独测试某个方法(定义,多线程,分治)时,采用文件输入的方式。
在进行性能测试的时候,为了测试方便,只讨论方阵,假设A是n*n阶,B也是n*n阶,那么要计算乘积需要进行n^2个元素。
- 输入文件格式:
- 第1行:线程数l的值(4,8,16,32,64,128)
- 第2行:矩阵A的行数m的值(32,64,128等)
- 第3行:矩阵A的列数n的值(32,64,128等)
- 第4行:矩阵B的列数k的值(32,64,128等)(默认矩阵B的行数为n)
- 接下来顺序输入矩阵A和矩阵B,按行输入(例:第5行输入矩阵A的第一行,以此类推)
- 输入文件示例:
- 输出内容:
- 根据不同的测试项目输出内容有所不同
- 读文件相应代码:
file = "mul_arr1.txt"
m = int(linecache.getline(file, 2))
n = int(linecache.getline(file, 3))
print("Please enter the rows and columns of matrix A:", m, n)
k = int(linecache.getline(file, 4))
print("Please enter the columns of matrix B:", k)
c = 5 # 记录读入文件行号
for i in range(0, m):
A.append(list(map(int, linecache.getline(file, c).rstrip().split())))
c = c + 1
for i in range(0, n):
B.append(list(map(int, linecache.getline(file, c).rstrip().split())))
c = c + 1
A = np.array(A)
B = np.array(B) # 将数组变为矩阵
print("Please enter matrix A:\n", A)
print("Please enter matrix B:\n", B)
1.手工方法计算乘法矩阵
1.1任意的 m**n 阶矩阵A和nk阶矩阵B核心代码(附件1):
B = B.T # 矩阵B转置
# 两个长度为n的一维数组相乘,计算结果进入队列
def mul_arr(M, N):
C = 0
for I in range(n):
C = C + M[I] * N[I]
q.put(C)
# 循环计算矩阵A的某一行和矩阵B的某一列的乘积,结果进入队列
def mul_arr0():
for i in range(0, m):
for j in range(0, k):
mul_arr(A[i], B[j])
说明:上述计算方法对应输入文件为文章开头示例文件。
- 输出文件示例:
1.2任意的两个m阶矩阵乘法用于测试代码运行效率(附件2):
from matplotlib.font_manager import FontProperties
import time
import matplotlib.pyplot as plt
import numpy as np
n = [2**2, 2**3, 2**4, 2**5, 2**6, 2**7, 2**8]
Sum_time1 = []
"""
定义法
-------------------------------------------------------------------------
"""
for m in n:
A = np.random.randint(1, 3, [m, m])
B = np.random.randint(1, 3, [m, m])
A1 = np.mat(A)
B1 = np.mat(B)
C2 = np.zeros([m, m], dtype=np.int)
time_start = time.time()
for i in range(m):
for k in range(m):
for j in range(m):
C2[i, j] = C2[i, j] + A[i, k] * B[k, j]
time_end = time.time()
Sum_time1.append(time_end - time_start)
f0 = open('python_time1.txt', 'w')
for ele in Sum_time1:
f0.writelines(str(ele) + '\n')
f0.close()
print(list(zip(n,Sum_time1)))
font = FontProperties(fname=r"c:\windows\fonts\simsun.ttc", size=28)
plt.semilogx(n, Sum_time1, 'b-*')
plt.ylabel(u"时间(s)", fontproperties=font)
plt.xlabel(u"矩阵的维度n", fontproperties=font)
plt.title(u'定义法', fontproperties=font)
plt.show()
- 性能测试结果
运行时间:
矩阵维数(n) | 运行时间(s) |
---|---|
4 | 0.0 |
8 | 0.0009908676147460938 |
16 | 0.0050160884857177734 |
32 | 0.037860870361328125 |
64 | 0.4158897399902344 |
128 | 3.467728614807129 |
256 | 27.43764090538025 |
结论:当矩阵维数达到128以上,矩阵乘法运行时间明显增快,矩阵维数为256阶时运行时间已达到27秒。 根据定义法的计算方法可知,定义法的矩阵乘法运算时间复杂度为O(n^3)
2.分治法计算乘法矩阵
2.1普通递归分治法计算矩阵乘法(附件3): 核心代码:
n = [2**2, 2**3, 2**4, 2**5, 2**6, 2**7, 2**8, 2**9, 2**10, 2**11]
Sum_time4 = []
for m in n:
A = np.random.randint(1, 3, [m, m])
B = np.random.randint(1, 3, [m, m])
A1 = np.mat(A)
B1 = np.mat(B)
"""
分治法
-------------------------------------------------------------------------
"""
A11 = np.mat(A[0:m // 2, 0:m // 2])
A12 = np.mat(A[0:m // 2, m // 2:m])
A21 = np.mat(A[m // 2:m, 0:m // 2])
A22 = np.mat(A[m // 2:m, m // 2:m])
B11 = np.mat(B[0:m // 2, 0:m // 2])
B12 = np.mat(B[0:m // 2, m // 2:m])
B21 = np.mat(B[m // 2:m, 0:m // 2])
B22 = np.mat(B[m // 2:m, m // 2:m])
time_start = time.time()
C11 = A11 * B11 + A12 * B21
C12 = A11 * B12 + A12 * B22
C21 = A21 * B11 + A22 * B21
C22 = A21 * B12 + A22 * B22
C3 = np.vstack((np.hstack((C11, C12)), np.hstack((C21, C22))))
time_end = time.time()
- 性能测试结果
运行时间:
矩阵维数(n) | 运行时间(s) |
---|---|
4 | 0.0 |
8 | 0.0 |
16 | 0.0009982585906982422 |
32 | 0.0 |
64 | 0.0 |
128 | 0.001994609832763672 |
256 | 0.015954971313476562 |
512 | 0.2533752918243408 |
1024 | 2.6089890003204346 |
2048 | 69.85507392883301 |
结论:采用递归分治算法并没有改变矩阵乘法的时间复杂度,但是在矩阵维数较少(<1000)时,计算时间明显较定义法时间短,即采用了分治的策略减少了计算时间。
2.2Strasses法计算矩阵乘法(附件4):
核心代码:
A11 = np.mat(A[0:m // 2, 0:m // 2])
A12 = np.mat(A[0:m // 2, m // 2:m])
A21 = np.mat(A[m // 2:m, 0:m // 2])
A22 = np.mat(A[m // 2:m, m // 2:m])
B11 = np.mat(B[0:m // 2, 0:m // 2])
B12 = np.mat(B[0:m // 2, m // 2:m])
B21 = np.mat(B[m // 2:m, 0:m // 2])
B22 = np.mat(B[m // 2:m, m // 2:m])
time_start = time.time()
M1 = A11 * (B12 - B22)
M2 = (A11 + A12) * B22
M3 = (A21 + A22) * B11
M4 = A22 * (B21 - B11)
M5 = (A11 + A22) * (B11 + B22)
M6 = (A12 - A22) * (B21 + B22)
M7 = (A11 - A21) * (B11 + B12)
C11 = M5 + M4 - M2 + M6
C12 = M1 + M2
C21 = M3 + M4
C22 = M5 + M1 - M3 - M7
C4 = np.vstack((np.hstack((C11, C12)), np.hstack((C21, C22))))
- 性能测试结果
运行时间:
矩阵维数(n) | 运行时间(s) |
---|---|
4 | 0.0009989738464355469 |
8 | 0.0 |
16 | 0.0 |
32 | 0.0 |
64 | 0.0009961128234863281 |
128 | 0.00299072265625 |
256 | 0.019945859909057617 |
512 | 0.1954805850982666 |
1024 | 2.3666739463806152 |
2048 | 60.793461561203 |
结论:Strassen算法进行了18次加法和7次乘法。运行时间为O(nlog7)=O(n2.81)。
当矩阵维数较少时,可能会因为需要划分矩阵而需要额外的运行时间,当矩阵的维数逐渐增大时,Strassen算法的运行时间明显小于定义法和递归分治法。
3.多线程(进程)计算乘法矩阵
3.1利用python自带的threadingpool库函数的线程池方法(附件5):
核心代码:
# 两个长度为n的一维数组相乘,计算结果进入队列
def mul_arr(M, N, q):
C = 0
for i in range(n):
C = C + M[i] * N[i]
q.put(C)
# 单个线程执行的任务,执行矩阵A分给第h(从1开始)个线程的任务,从A的(h-1)*f行计算到h*f-1行
def single_thread(h, q):
print(threading.current_thread().name)
for i in range((h - 1) * f, (h * f)):
for j in range(0, k):
mul_arr(A[i], B[j], q)
if __name__ == '__main__':
pool = ThreadPoolExecutor(max_workers=l)
start = time.time()
for i in range(1, l + 1):
task = pool.submit(single_thread, i, q) # 提交任务到线程池
task.done()
pool.shutdown()
stop = time.time()
print("Exiting Main Thread.%s" % ctime())
print("The total time consumption is %s secs" % (stop - start))
results = []
for i in range(m * k):
results.append(q.get())
results = np.array(results)
results.resize((m, k))
print("Multithreading is used to compute the matrix A multiplied by the matrix B is:")
print(results)
-
运行结果:
-
结果分析:运行发现,利用Python多线程计算矩阵乘法时间反而大于普通的计算方法(1.1638882160186768>0.6702075004577637)。
原因是由于Python自带的全局解释器锁(GIL),导致python在运行多线程时,虽然Python解释器可以运行多个线程,但是只有一个线程在解释器中运行,因此对于矩阵的计算仍然为串行执行,并且由于启用线程和线程切换,反而增加了时间开销。改进方案:利用python多进程实现多任务的多核并行处理。
3.2利用python的多进程multiprocessing库函数实现矩阵乘法计算(附件6):
3.2.1核心代码:
Sum_time1 = time_end - time_start
print("矩阵的维数n:", m)
print("进程数:", l)
pool = multiprocessing.Pool(processes=4) # 创建4个进程
pool_list = []
result_list = []
print("本机为服务器T", np.os.cpu_count(), "核 CPU")
start_t = time.time()
for i in range(1, l+1): # 启动l个进程
r = pool.apply_async(single_thread, args=(i,)) # 产生一个非同步进程,函数newsin的参数用args传递
result_list.append(r) # 将返回结果放入results
pool.close() # 关闭进程池
pool.join() # 结束
- 运行结果:
3.2.2性能比较:
如图所示,为多进程实现不同维度矩阵乘法计算的时间性能比较:
- 结果分析:
从表格中可以看出,对于计算维度比较小的矩阵(比如小于64维),多进程相较于定义法没有取得很好地效果,原因是,此时进程的创建和调度多花费的时间占比相较于计算矩阵的时间较大。
多进程利用多核CPU完成任务,是操作系统资源分配的最小单位,进程拥有自己独立的内存空间,所以进程间数据不共享,进程之间的通信由操作系统传递,导致通讯效率低,切换开销大,创建或销毁进程时系统开销大。
对于计算维数较大的密集型任务时,多进程会表现出较好的性能。
由于本程序运行的环境,CPU为4核,从表格分析可以看出,当进程数量取4较为合适。
3.3利用Java的多线程实现矩阵乘法的并行计算(附件7):
3.3.1核心代码:
package mul_arr;
import java.util.concurrent.CountDownLatch;
public class CalculateTask extends Thread {
private int[][] A;
private int[][] B;
private int index;//子线程的索引值,即第几个线程
private int f; //每个子线程计算矩阵A的行数
private int[][] result;
private CountDownLatch countDownLatch;
public CalculateTask(int[][] A, int[][] B, int index, int f, int[][] result, CountDownLatch countDownLatch) {
this.A = A;
this.B = B;
this.index = index;
this.f = f;
this.result = result;
this.countDownLatch = countDownLatch;
}
// 计算特定范围内的结果
public void run() {
// TODO Auto-generated method stub
for (int i = index * f; i < (index + 1) * f; i++)
for (int j = 0; j < B[0].length; j++) {
for (int k = 0; k < B.length; k++)
result[i][j] += A[i][k] * B[k][j];
}
// 线程数减1
countDownLatch.countDown();
}
public static void main(String[] args) throws InterruptedException {
// 声明和初始化
long startTime;
long endTime;
int m = 64;//矩阵A的行
int n = 64;//矩阵A的列
int k = 64;//矩阵B的列
int[][] A = new int[m][n];
int[][] B = new int[n][k];
System.out.println("矩阵A的行:" + m );
System.out.println("矩阵A的列:" + n );
System.out.println("矩阵B的列:" + k );
//存放多线程计算结果
int[][] mul_result = new int[A.length][B[0].length];
//存放定义法计算结果
int[][] general_result = new int[A.length][B[0].length];
//子线程数量
int threadNum = 4;
System.out.println("线程数:" + threadNum );
//子线程计算的矩阵行数
int f = A.length / threadNum;
// 矩阵初始化
for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++) {
A[i][j] = 1;
}
for (int i = 0; i < n; i++)
for (int j = 0; j < k; j++) {
B[i][j] = 2;
}
// 多线程计算
CountDownLatch countDownLatch = new CountDownLatch(threadNum);
startTime = System.nanoTime();
for (int i = 0; i < threadNum; i++) {
CalculateTask ct = new CalculateTask(A, B, i, f, mul_result, countDownLatch);
ct.start();
}
countDownLatch.await();
endTime = System.nanoTime();
System.out.println("多线程Sum_time:" + (endTime - startTime) + "ns");
// 定义法计算
startTime = System.nanoTime();
for (int i = 0; i < A.length; i++) {
for (int j = 0; j < B[0].length; j++) {
for (int w = 0; w < A[0].length; w++)
general_result[i][j] += A[i][w] * B[w][j];
}
}
endTime = System.nanoTime();
System.out.println("定义法Sum_time:" + (endTime - startTime)+"ns");
}
}
3.3.2性能分析:
-
运行结果:
-
JAVA多线程性能比较
- 结果分析:由上图可知,并不是线程数越多越好,对于我本人的程序,操作系统环境内核为4,最佳线程数应该为4。