单线程计算矩阵相乘

开发一个单线程的程序,分别计算不同规模矩阵的乘积,并记录矩阵相乘花费的时间;

从文件中读取矩阵的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void readMatrix(double[][] matrix, String fileName) {
String pathName = "./mx/" + fileName + ".txt";
try (FileReader fr = new FileReader(pathName); BufferedReader br = new BufferedReader(fr)) {
String line;
int cntLine = 0;
while ((line = br.readLine()) != null) {
String[] nums = line.split(" ");
for (int i = 0; i < nums.length; i++) {
matrix[cntLine][i] = Double.valueOf(nums[i]).doubleValue();
}
cntLine++;
}
} catch (IOException e) {
e.printStackTrace();
}
}

两矩阵相乘的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static double[][] multiple(double[][] matrixA, double[][] matrixB) {
// 串行进行矩阵乘法
int rows = matrixA.length;
int cols = matrixA[0].length;
double[][] res = new double[rows][cols];
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
for (int k = 0; k < cols; k++) {
res[i][j] += matrixA[i][k] * matrixB[k][j];
}
}
}
return res;
}

main函数

  • 根据不同的规格读取矩阵文件
  • 进行矩阵乘法计算
  • 输出计算的耗时
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
int[] sizes = {64,128,512,1024};
Date start,end;
double[][] matrixA= new double[size][size];;
double[][] matrixB= new double[size][size];;
for(int i = 0;i<sizes.length;i++){
size = sizes[i];
matrixA = new double[size][size];
matrixB = new double[size][size];
start = new Date();
readMatrix(matrixA, "M" + size + "A");
readMatrix(matrixB, "M" + size + "B");
double[][] ans = multiple(matrixA, matrixB);
end = new Date();
System.out.println("单线程串行计算" + size + "x" + size + "矩阵乘法耗时:" + (end.getTime() - start.getTime()) + "ms");
}
}

运行结果

image-20201110225637263

结果分析

算法复杂度分析

  • 矩阵相乘的时间复杂度为O(N³)

  • 64x64矩阵相乘大约需要运算260 000次=2.6x10^5^

  • 128x128矩阵相乘大约需要运算2 100 000次=2.1x10^6^

  • 512x512矩阵相乘大约需要运算130 000 000次=1.3x10^8^

  • 1024x1024矩阵相乘大约需要运算1 070 000 000次=1.07x10^9^

运行环境分析

  • 笔记本CPU i5-8250U 四核八线程 主频1.6GHz = 每秒计算上限为1.6x10^9^

  • 由于当前是单线程运行,所以理论上限应该是每秒运算2x10^8^次

  • 加上笔记本CPU会自动降频以减少能耗,所以实际的运算速度会比理论上限低

运行结果分析

  • 各规格矩阵相乘耗时 基本和 理论计算值÷理论计算速度 吻合

四线程计算矩阵相乘

开发一个 4 线程的程序,分别计算 16x16 128x128 512x512 矩阵的乘积,并记录矩阵相乘花费的时间;

定义运算线程类

  • 每个线程需要被分配各自的任务来实现多线程矩阵计算
  • 比如新建2个线程,一个矩阵应该负责1,3,5…列的计算,另一个矩阵负责2,4,6…列的计算
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class MatrixThread implements Runnable {
private int start;
private double[][] A;
private double[][] B;
private double[][] C;

public MatrixThread(int start, double[][] A, double[][] B, double[][] C) {
this.start = start;
this.A = A;
this.B = B;
this.C = C;
}

@Override
public void run() {
// 根据线程数量划分线程任务
for (int i = start; i < MatrixMultiple.size; i += MatrixMultiple.totalThreads) {
for (int j = 0; j < MatrixMultiple.size; j++) {
for (int k = 0; k < MatrixMultiple.size; k++) {
C[i][j] += A[i][k] * B[k][j];
}
}
}
}
}

使用所定义的线程计算矩阵

  • 建立线程数组
  • 初始化每个线程
  • 启动每个线程
  • 在线程运行完之后终止线程
  • 检验多线程计算的矩阵结果和单线程计算的结果是否相同
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public static void main(String[] args) {
int[] sizes = { 64, 128, 512, 1024 };
Date start, end;
double[][] matrixA = new double[size][size];
double[][] matrixB = new double[size][size];
for (int k = 0; k < sizes.length; k++) {
size = sizes[k];
matrixA = new double[size][size];
matrixB = new double[size][size];
start = new Date();
readMatrix(matrixA, "M" + size + "A");
readMatrix(matrixB, "M" + size + "B");
double[][] ans = multiple(matrixA, matrixB);
end = new Date();
long time1 = end.getTime() - start.getTime();
System.out.println("单线程串行计算" + size + "x" + size + "矩阵乘法耗时:" + time1 + "ms");

Thread[] threads = new Thread[totalThreads]; // 线程组
double[][] matrixC = new double[size][size]; // 存放答案数组
for (int i = 0; i < totalThreads; i++) {
threads[i] = new Thread(new MatrixThread(i, matrixA, matrixB, matrixC)); // 初始化每个线程
}
start = new Date();
for (int i = 0; i < totalThreads; i++) {
threads[i].start(); // 启动每个线程
}
for (int i = 0; i < totalThreads; i++) {
try {
threads[i].join(); // 当线程结束时就终止线程
} catch (Exception e) {
e.printStackTrace();
}
}
end = new Date();
long time2 = end.getTime() - start.getTime();
System.out.println(
totalThreads + "线程并行计算" + size + "x" + size + "矩阵乘法耗时:" + time2 + "ms");

double rate = 1.0*(time1-time2)/time1;
System.out.printf(
totalThreads + "线程并行计算" + size + "x" + size + "矩阵乘法的节约时间比例为%.2f\n", rate);
// 对多线程计算结果进行检验
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
if (ans[i][j] != matrixC[i][j]) {
System.out.println("第" + i + "行" + "第" + j + "列不相等");
}
}
}
System.out.println();
}
}

运行结果

  • 多次运行以避免偶然性和减少误差

当前耗时只考虑多线程计算的时间,没有把创建线程和初始化线程的时间算进去

  • 可以看到四线程运行并不能跑满CPU利用率
image-20201126230925233

结果分析

  • 可以看到由于CPU最大支持8线程,所以在4线程的情况下是能做到并行的
    • 4线程并行情况下运算耗时大约为单线程情况下的四分之一
    • 矩阵规模越大,节约比例越接近0.75
  • 这里我只用了四个线程进行计算,但是却可以发现这边显示的CPU0-7的利用率都同时提升了
    • 按理来说,4个线程应该最多只能在4个核上运行
    • 原因是,事实上这个CPU在物理上就是4个内核,只是在逻辑上抽象成了8个逻辑处理器
    • 可以理解为我把现在雇佣了四个厨师,每个厨师可以同时两只手做两个菜,即左手做一份菜,右手做一份菜
    • 现在我派这四个厨师每人做一份菜
    • 他们现在很省力,因为可以左手做半份菜,右手做半份菜
    • 这对于他们来说只需要花费50%的力气就足够了

十六线程计算矩阵相乘

  • 定义线程类和计算代码类似,在此不再赘述
  • 只需要改线程数即可 public static int totalThreads = 16;

运行结果

  • 多次运行以避免偶然性和减少误差
  • 可以在任务管理器中明显看到,在进行多线程计算时,CPU利用率显著提高,甚至直接跑满了
  • 利用win10自带的资源监视器也可以很明显地看到了是8个核的利用率都满了

结果分析

  • 16线程的理论节约时间比例应该是$15/16 = 0.9375$
    • 但是实际上由于CPU只支持最大8线程并行,所以即使创建了16线程,也没有能做到16线程并行
    • 所以理论节约时间比例的上限只有$7/8 = 0.875$
  • 可以看到这次运行时间节约比例和理论值差的很多
    • 16线程(实际是8线程)计算的所花的时间甚至比4线程计算的时间还要长
    • 个人理解是JVM在线程调度的时候开销增加,导致16进程计算矩阵相乘的时候浪费了大量时间进行线程方面的操作
    • 当然由于本身系统环境本身也不是不变的,所以也有可能是其他进程对该程序的影响。
  • 在这个情况下,这四个厨子(四个物理核心)
    • 我让他们每人做两份菜
    • 他们每人左手做一份,右手做一份(每个核心抽象成两个逻辑处理器)
    • 因此他们已经达到了自己的极限

线程学习总结

  1. 合理利用多线程可以有效的增加计算速度
  2. 相对与单线程来说,多线程的编程会更加复杂
  3. 过多的线程数并不一定能节省时间,可能会导致计算时间的增加