前一段时间在写一门课的作业,在作业的过程中发现了一些问题,也学到了一些平时注意不到的知识点。正好忙里偷闲,打算记录一下。
这门课的名字是“高性能智能计算实验”,虽然我之前没接触过类似实验,但是可选的实验课里面只有这门课是身边同门都选的,而且算是可选范围中能知道大致内容的课。
题目:使用Pthread库实现多线程矩阵乘法(Linux或者windows均可),变化矩阵大小并分析不同线程个数下的加速比和计算效率。
当然,上面的题目只是作业里面的一部分,这部分让我有所发现和了解,所以选择记录一下。
由于一开始不太明白,所以就先在网上搜索一番,找到了一个简化版的代码作为起始代码。
#include
#include
#include
int thread_count;
int size, local_size;
int *a, *b, *c;
FILE *fp;
int* transpose_matrix(int *m, int size);
void* Init();
void* pthread_mult(void* rank);
int main(int argc, char* argv[]){
int i, j;
long thread;
float time_use = 0;
struct timeval start;
struct timeval end;
size = 1000;
gettimeofday(&start, NULL);
pthread_t* thread_handles;
thread_count = strtol(argv[1], NULL, 10);
local_size = size/thread_count;
thread_handles = malloc(thread_count * sizeof(pthread_t));
Init();
for(thread = 0; thread<thread_count; thread++)
pthread_create(&thread_handles[thread], NULL, pthread_mult, (void*) thread);
for (thread=0; thread<thread_count; thread++)
pthread_join(thread_handles[thread], NULL);
fp=fopen("c.txt","w");//打开文件
for(i=0;i<1000;i++) {//写数据
for(j=0;j<1000;j++)
fprintf(fp,"%d ",c[i*size+j]);
fputc('\n',fp);
}
fclose(fp);//关闭文件
gettimeofday(&end, NULL);
time_use = (end.tv_sec-start.tv_sec)*1000000+(end.tv_usec-start.tv_usec);
printf("time_use is %f\n", time_use/1000000);
free(thread_handles);
free(a);
free(b);
free(c);
return 0;
}
int* transpose_matrix(int *m, int size){
int i, j;
for(i=0; i<size; i++){
for(j=i+1; j<size; j++){
int temp = m[i*size+j];
m[i*size+j] = m[j*size+i];
m[j*size+i] = temp;
}
}
return m;
}
void* Init(){
int i, j;
a = (int*)malloc(sizeof(int)*size*size);
b = (int*)malloc(sizeof(int)*size*size);
c = (int*)malloc(sizeof(int)*size*size);
//从文件中读入矩阵
fp=fopen("a.txt","r");//打开文件
for(i=0;i<1000;i++) //读数据
for(j=0;j<1000;j++)
fscanf(fp,"%d",&a[i*size+j]);
fclose(fp);//关闭文件
fp=fopen("b.txt","r");
for(i=0;i<1000;i++)
for(j=0;j<1000;j++)
fscanf(fp,"%d",&b[i*size+j]);
fclose(fp);
b = transpose_matrix(b, size);
}
void* pthread_mult(void* rank){
long my_rank = (long) rank;
int i, j, k, temp;
int my_first_row = my_rank*local_size;
int my_last_row = (my_rank+1)*local_size - 1;
for(i = my_first_row; i <= my_last_row; i++){
for(j = 0; j<size; j++){
temp = 0;
for(k = 0; k<size; k++)
temp += a[i*size+k] * b[j*size+k];
c[i*size+j] = temp;
}
}
}
// https://blog.csdn.net/lcx543576178/article/details/45893101
运行代码,发现缺少矩阵文件,所以我又自己写了一个生成矩阵的python代码。
import numpy as np
MAXNUM=100 #设置矩阵元素的最大值
MINNUM=0 #设置矩阵元素的最小值
M=8000 #设置A矩阵的行数
N=6000 #设置A矩阵的列数
P=8000 # 设置B矩阵的列数
randomMatrixA=np.random.randint(MINNUM,MAXNUM,(M,N))
np.savetxt(r'./8000-6000-8000A.txt',randomMatrixA,fmt="%d", delimiter=' ')
randomMatrixB=np.random.randint(MINNUM,MAXNUM,(N,P))
np.savetxt(r'./8000-6000-8000B.txt',randomMatrixB,fmt="%d", delimiter=' ')
randomMatrixC=np.dot(randomMatrixA,randomMatrixB)
np.savetxt(r'./AB.txt',randomMatrixC,fmt="%d", delimiter=' ')
上述代码可以设定矩阵的行数、列数和值的范围,然后随机生成后,写入文件,方便c程序读取。
然后运行矩阵乘并行程序代码,发现可以正常运行输出结果。为了校验计算结果的正确性,我又写了一个python脚本,用来检验矩阵相乘结果是否正确。代码如下:
import numpy as np
ab = np.loadtxt("AB.txt")
print(ab)
c = np.loadtxt("c.txt")
print(c)
print((ab==c).all())
检测结果也正确,所以这个代码基本上是正确的。
但是,我发现初始代码较为繁杂,而且好像只是针对单一的矩阵维度进行计算,所以我在其基础上进行了改进,添加矩阵维度设定参数,同时修改了计算时间的代码。
#include
#include
#include
#include
# define M 2000
# define N 1500
# define P 2000
int thread_count;
int sub_size;
int *a, *b, *c;
FILE *fp;
// read matrix from text and convert matrix to transposed matrix
void* Init(){
int i, j;
a = (int*)malloc(sizeof(int)*M*N);
b = (int*)malloc(sizeof(int)*N*P);
int *temp = (int*)malloc(sizeof(int)*P*N);
c = (int*)malloc(sizeof(int)*M*P);
fp=fopen("2000-1500-2000A.txt","r");
for(i=0;i < M;i++)
for (j = 0; j < N; j++) {
fscanf(fp, "%d", &a[i * N + j]);
}
fclose(fp);
fp=fopen("2000-1500-2000B.txt","r");
for(i=0;i<N;i++)
for(j=0;j<P;j++)
fscanf(fp,"%d", &temp[i * P + j]);
fclose(fp);
for(i=0; i<P; i++){
for(j=0; j<N; j++){
b[i*N+j] = temp[j*P+i];
}
}
free(temp);
}
// main part of multiply
void* pthread_multiply(void* num){
long sub_num = (long) num; // num means the rank of the sub thread
int i, j, k, temp;
int sub_start_row = sub_num*sub_size; // subline of this thread
int sub_end_row = (sub_num+1)*sub_size - 1; // endline of this thread
// calculate every line of A multiply B of this thread
for(i = sub_start_row; i <= sub_end_row; i++){
for(j = 0; j<P; j++){
temp = 0;
for(k = 0; k<N; k++)
temp += a[i*N+k] * b[j*N+k]; // multiply of every line
c[i*P+j] = temp;
}
}
}
int main(int argc, char* argv[]){
int i, j;
long thread;
clock_t start_t, end_t;
double sum_t = 0;
thread_count = 8; // number of thread
sub_size = M/thread_count; // number of line of every subthread
pthread_t* thread_handles;
thread_handles = malloc(thread_count * sizeof(pthread_t));
Init();
struct timeval begin, end;
// start_t = clock();
gettimeofday(&begin, NULL);
for(thread = 0; thread<thread_count; thread++)
pthread_create(&thread_handles[thread], NULL, pthread_multiply, (void*) thread);
for (thread=0; thread<thread_count; thread++)
pthread_join(thread_handles[thread], NULL);
gettimeofday(&end, NULL);
printf("time: %lld\n", ((long long) end.tv_sec*1000+(long long) end.tv_usec/1000)-((long long) begin.tv_sec*1000+(long long) begin.tv_usec/1000));
end_t = clock();
fp=fopen("8000-6000-8000c.txt","w");
for(i=0;i<M;i++) {
for(j=0;j<P;j++)
fprintf(fp,"%d ",c[i*P+j]);
fputc('\n',fp);
}
fclose(fp);
// sum_t = ((double) (end_t -start_t)) / CLOCKS_PER_SEC;
// printf("time: %f\n", sum_t);
free(thread_handles);
free(a);
free(b);
free(c);
return 0;
}
上面的代码基本上是最后一版,能够设定不同维度的矩阵参数,可以保存输出。同时注释了几行代码,那部分是计算时间的另一种方式,由于计算准确度难以确定,所以我换成了另一种即没注释的代码。
到此为止,代码撰写基本工作算是结束了,接下来是测试时间。本以为这部分是比较简单的,但是出现了坑让我止步不前。
首先是在实验室电脑上测试的,用Clion编译运行,然后输出结果。但是我发现在最小的矩阵大小面前,测试时间都是比较长的,而且改变线程数对测试时间也没任何影响。
前者还能接收,因为可能有一些潜在因素导致运行时间长,但是后者线程数增加时间还不变化,那我这实验测试还有什么意思?
在实验室电脑折腾一番,分别在解释器内编译运行和在命令行下编译运行,结果都是如此。短暂的迷茫期之后,我考虑用我自己的笔记本测试一番。
放到我笔记本上,用dev c++打开,然后编译运行,测试结果居然提升了好几倍。此时我怀疑是实验室电脑的问题,因为我把Ubuntu系统装到了机械硬盘上,让我怀疑是不是这个原因。但是由于当时后面就是假期,所以就没考虑那么多,打算在假期期间之间用自己的笔记本做实验。当然,实验结果都较为理想,因为都是用dev c++调试运行的,一方面方便修改代码,另一方面看起来也比我用记事本舒服。
收假后,前两天回到实验室,把实验室电脑装的Ubuntu系统文件迁移到了固态硬盘上,花费了一部分时间和精力,不过最后还是成功迁移。如果有想做同样事的小伙伴可以参考这位博主的文章。
https://blog.csdn.net/Bruski/article/details/115840667
对于有迁移系统盘需求的小伙伴,这篇介绍还是较为详细的,可以参考操作。
系统迁移后,我又重新运行了一下之前的程序,发现结果还是那么慢。所以后面我又重新研究dev c++了。
至此算是发现了比较致命的问题,时间上总是不太对。
通过观察dev c++的输出,我发现它在编译的时候,加上静态库编译,同时加入了一个参数-O2。
经过查找资料,我发现-O2是gcc的一个优化编译选项,类似的还有-O1,-O3。
-O1就是比较基础的优化,对于常规的分支、循环等进行优化。
-O2是对深层次指令进行优化,实现编译时间的加速。
-O3是在-O2的基础上继续优化,进一步加速运行。
所以,当在windows上分别用命令行和dev c++进行编译运行时,时间其实也是不同的。
看起来问题好像是解决了,但是,我又在实验室电脑上Ubuntu上进行了一番新测试,我发现输出的时间比我观察到的时间要长,我自己数大概就是3-4s,但是输出12s。于是我换了一种时间记录方式,就是最后一版代码中没注释的记录方法。通过打印结果的方式,我发现两种计算方法记录输出的时间是有差距的,而且差距很大。不过能够看出,新的方式记录的时间是符合预期的。
这里就很奇怪了,直觉上告诉我,应该是在Ubuntu下的时间记录方式出了问题。
PS D:\desktop> gcc .\main.c -o main -lpthread
PS D:\desktop> ./main
time: 2679 4885 2.206000 2
time: 2206
PS D:\desktop> ./main
time: 2666 4827 2.161000 2
time: 2161
PS D:\desktop> ./main
time: 2668 4900 2.232000 2
time: 2231
PS D:\desktop> ./main
time: 2671 4863 2.192000 2
time: 2192
可以看出在windows下两种时间记录方式的记录结果是一致的。
但是,在ubuntu下的测试结果是不一致的。为此,我查阅相关资料。
经过了解,明白了clock()方式是在记录CPU的使用时间,不是具体的现实时间消耗。但是我并没有找到具体的解释,介绍为什么会这样。下面是百度文心一言给我的解释:
在Ubuntu和Windows中,clock()函数的实现可能会有些许差异,这可能导致在不同的操作系统上观察到不同的行为。尽管C和C++标准库提供了一个通用的clock()函数,但具体的实现可能因操作系统和编译器而异。
在Windows上,clock()函数通常使用的是Windows API中的高精度计时器,这可以提供非常准确的计时。而在Ubuntu上,clock()函数可能使用的是系统默认的计时器,这可能不如Windows API的高精度计时器准确。
此外,还有一些其他因素可能会导致在Ubuntu上使用clock()函数时出现不准确的情况:
1. 系统负载:如果Ubuntu系统正在进行大量其他计算任务,这可能会影响计时器的准确度。相比之下,Windows系统可能会在计时器方面有更好的优化,以应对系统负载的情况。
2. 处理器时间片:在多任务操作系统中,处理器时间片分配可能对计时产生影响。如果一个进程(或线程)被分配的时间片过短,那么计时可能会不准确。
3. 系统调度:操作系统的调度策略也可能会影响计时。在某些情况下,如果系统调度频繁地打断进程的执行,那么计时可能会受到影响。
4. 系统时钟更新:如果系统时钟在计时期间被更新(例如,由于系统时间同步),那么这也可能会导致计时不准确。
如果你需要在Ubuntu上获得更准确的计时,你可能需要考虑使用其他计时方法。例如,你可以考虑使用Linux的高精度计时器(如clock_gettime()或gettimeofday()函数),或者使用特定于应用程序的计时方法(例如,使用std::chrono库)。这些方法可能提供更准确的计时,但它们可能需要特定的编程知识和技巧。
我对上述回答保留个人意见,如果有读者知道具体的原因,可以邮件给我进一步沟通。相互讨论问题钻研发现的问题,对我而言,也是人生的一种乐趣。
这篇文章主要介绍实验中遇到的问题,分为gcc -O2优化和c语言中clock()函数计时不准确的问题,希望能够给大家带去帮助。当然,希望大家能够对我文章中不正确的地方和有待改进的地方进行指出,毕竟分享知识也是为了互相学习。我的邮箱:[email protected]。大家有事情的话,一定要邮箱联系我,我基本上很少看后台留言,而且后台留言超过30天后就看不见了。
由于忙时在忙,闲事在摆,所以前一段时间就没有写文章,后面我会抽空继续分享知识,相互进步。