我有一个任务 - 比较2个矩阵乘法 - 默认方式,和第二个矩阵换位后的乘法,我们应该指出哪个方法更快的差异。我在下面写过类似的内容,但time
和time2
几乎相等。在一种情况下,第一种方法更快,我使用相同大小的矩阵运行乘法,而在另一种情况下,第二种方法更快。做错了吗?我应该改变代码中的内容吗?
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;
答案 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]
其中a
有L+1
列,b
有L+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]
请注意,a
和B
在内存中是连续的,但是可以分开(可能与#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; (除非它不是,因为你交换每个元素对两次,返回它们的方式),你的最内层循环超过最左边的数组索引,这意味着它计算错误的结果。 (此外,因为最内层循环的连续迭代在内存中访问远离彼此的项目,所以反优化:它在速度方面使用最差的模式。)
以上所有内容可能听起来都很苛刻,但并不是所有。我不认识你,我也不想评价你;我只是在你的当前理解中指出了这个特定答案中的错误,并且只希望它能帮助你和其他在类似情况下遇到这个问题的人学习。