矩阵乘法有两种不同的方式(比较时间)

时间:2017-12-25 10:56:30

标签: c matrix

我有一个任务 - 比较2个矩阵乘法 - 默认方式,和第二个矩阵换位后的乘法,我们应该指出哪个方法更快的差异。我在下面写过类似的内容,但timetime2几乎相等。在一种情况下,第一种方法更快,我使用相同大小的矩阵运行乘法,而在另一种情况下,第二种方法更快。做错了吗?我应该改变代码中的内容吗?

clock_t start = clock();

    int sum;
    for(int i=0; i<size; ++i) {
        for(int j=0; j<size; ++j) {
            sum = 0;
            for(int k=0; k<size; ++k) {
                sum = sum + (m1[i][k] * m2[k][j]);
            }
            score[i][j] = sum;
        }
    }

    clock_t end = clock();
    double time = (end-start)/(double)CLOCKS_PER_SEC;

    for(int i=0; i<size; ++i) {
        for(int j=0; j<size; ++j) {
            int temp = m2[i][j];
            m2[i][j] = m2[j][i];
            m2[j][i] = temp;
        }
    }

    clock_t start2 = clock();

    int sum2;
    for(int i=0; i<size; ++i) {
        for(int j=0; j<size; ++j) {
            sum2 = 0;
            for(int k=0; k<size; ++k) {
                sum2 = sum2 + (m1[k][i] * m2[k][j]);
            }
            score[i][j] = sum2;
        }
    }

    clock_t end2 = clock();
    double time2 = (end2-start2)/(double)CLOCKS_PER_SEC;

1 个答案:

答案 0 :(得分:0)

您的代码和/或您的理解存在多个严重问题。让我试着解释一下。

矩阵乘法受到处理器加载并将值存储到内存的速率的瓶颈。大多数当前架构使用 cache 来帮助解决这个问题。数据从内存移动到缓存,从缓存移动到内存中。为了最大限度地利用缓存,您需要确保使用该块中的所有数据。为此,请确保在内存中按顺序访问数据

在C中,多维数组在row-major order中指定。这意味着最右边的索引在内存中是连续的;即a[i][k]a[i][k+1]在记忆中是连续的。

根据体系结构,处理器等待(并且什么都不做)将数据从RAM移动到缓存(反之亦然)所花费的时间可能包括也可能不包括在CPU时间中(例如clock()措施,尽管分辨率非常低)。对于这种测量(&#34; microbenchmark&#34; ),测量和报告使用的CPU和实际(或挂钟)时间要好得多;特别是如果微基准测试在不同的机器上运行,以更好地了解变化的实际影响。

会有很多变化,所以通常情况下,你会测量几百次重复所花费的时间(每次重复可能会进行多次操作;足以轻松测量),存储每次重复的持续时间,并报告他们的中值。为什么中位数,而不是最小值,最大值,平均值?因为总会偶尔出现故障(由于外部事件或其他因素导致的不合理测量),这通常产生比正常情况高得多的值;除非删除,否则这会使最大程度无趣,并使平均值(平均值)偏斜。最低限度通常是过于乐观的情况,其中一切恰好都是完美的;这在实践中很少发生,所以只是好奇心,而不是实际的兴趣。另一方面,中位时间为您提供了一个实际测量:您可以预期测试用例的所有运行中的50%不会超过测量的中位时间。

在POSIXy系统(Linux,Mac,BSD)上,您应该使用clock_gettime()来衡量时间。 struct timespec格式具有纳秒精度(1秒= 1,000,000,000纳秒),但分辨率可能更小(即,每当它们改变时,时钟变化超过1纳秒)。我个人使用

#define _POSIX_C_SOURCE 200809L
#include <time.h>

static struct timespec  cpu_start, wall_start;
double                  cpu_seconds, wall_seconds;

void timing_start(void)
{
    clock_gettime(CLOCK_REALTIME, &wall_start);
    clock_gettime(CLOCK_THREAD_CPUTIME_ID, &cpu_start);
}

void timing_stop(void)
{
    struct timespec  cpu_end, wall_end;
    clock_gettime(CLOCK_REALTIME, &wall_end);
    clock_gettime(CLOCK_THREAD_CPUTIME_ID, &cpu_end);

    wall_seconds = (double)(wall_end.tv_sec - wall_start.tv_sec)
                 + (double)(wall_end.tv_nsec - wall_start.tv_nsec) / 1000000000.0;
    cpu_seconds = (double)(cpu_end.tv_sec - cpu_start.tv_sec)
                + (double)(cpu_end.tv_nsec - cpu_start.tv_nsec) / 1000000000.0;
}

您在操作前调用timing_start(),在操作后调用timing_stop();然后,cpu_seconds包含所花费的CPU时间量和wall_seconds实际挂钟时间(以秒为单位,使用例如%.9f来打印所有有意义的小数)。

以上不会在Windows上工作,因为Microsoft不希望您的C代码可以移植到其他系统。它更喜欢发展自己的标准&#34;代替。 (那些C11和#34;安全&#34; _s() I / O函数变体是一个愚蠢的假,与例如POSIX getline()相比,或者除了Windows之外的所有系统上的宽字符支持状态。)

矩阵乘法是

c[r][c] = a[r][0] * b[0][c]
        + a[r][1] * b[1][c]
        :         :
        + a[r][L] * b[L][c]

其中aL+1列,bL+1行。

为了使求和循环使用连续元素,我们需要转置b。如果是B[c][r] = b[r][c],那么

c[r][c] = a[r][0] * B[c][0]
        + a[r][1] * B[c][1]
        :         :
        + a[r][L] * B[c][L]

请注意,aB在内存中是连续的,但是可以分开(可能与#34;远离彼此),以便处理器有效地利用缓存这种情况。

OP使用一个类似于以下伪代码的简单循环来转置b

For r in rows:
    For c in columns:
        temporary = b[r][c]
        b[r][c] = b[c][r]
        b[c][r] = temporary
    End For
End For

上面的问题是每个元素都参与交换两次。例如,如果b有10行和10列,r = 3, c = 5交换b[3][5]b[5][3],但稍后,r = 5, c = 3交换b[5][3]和{再次{1}}!基本上,双循环最终将矩阵恢复为原始顺序;它没有进行转置。

考虑以下条目和实际转置:

b[3][5]

不交换对角线条目。您只需要在上三角形部分(其中b[0][0] b[0][1] b[0][2] b[0][0] b[1][0] b[2][0] b[1][0] b[1][1] b[1][2] ⇔ b[0][1] b[1][1] b[2][1] b[2][0] b[2][1] b[2][2] b[0][2] b[1][2] b[2][2] )或下三角形部分(其中c > r)中进行交换,以交换所有条目,因为每个交换交换上三角形中的一个条目下三角形,反之亦然。

所以,回顾一下:

  

做错了什么?

是。你的转置什么都不做。你还没有理解人们想要转置第二个矩阵的原因。您的时间测量依赖于低精度CPU时间,这可能无法反映在RAM和CPU缓存之间移动数据所花费的时间。在第二个测试案例中,r > c&#34;转置&#34; (除非它不是,因为你交换每个元素对两次,返回它们的方式),你的最内层循环超过最左边的数组索引,这意味着它计算错误的结果。 (此外,因为最内层循环的连续迭代在内存中访问远离彼此的项目,所以反优化:它在速度方面使用最差的模式。)

以上所有内容可能听起来都很苛刻,但并不是所有。我不认识你,我也不想评价你;我只是在你的当前理解中指出了这个特定答案中的错误,并且只希望它能帮助你和其他在类似情况下遇到这个问题的人学习。