dev优化和clock()函数不准确问题

引入

前一段时间在写一门课的作业,在作业的过程中发现了一些问题,也学到了一些平时注意不到的知识点。正好忙里偷闲,打算记录一下。

实验过程和问题发现

这门课的名字是“高性能智能计算实验”,虽然我之前没接触过类似实验,但是可选的实验课里面只有这门课是身边同门都选的,而且算是可选范围中能知道大致内容的课。

题目:使用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天后就看不见了。

由于忙时在忙,闲事在摆,所以前一段时间就没有写文章,后面我会抽空继续分享知识,相互进步。

你可能感兴趣的:(c语言)