矩阵乘法cache优化

好文要转,太棒了~~~~~~~~~~~~~~~~~~~~~~~~~

题目地址:http://www.51nod.com/onlineJudge/questionCode.html#!problemId=1113

昨晚为了优化这个题目弄到2点多,今天一早就写博,我真是太不蛋定了,哈哈。

做OJ的朋友都知道快速幂,我就不罗嗦了,我说的主要是矩阵乘法实现层面的优化。

最开始我的代码耗时1156ms,代码如下:

  1. void  mat_mul( int (*a)[MAXN], int (*b)[MAXN], int (*c)[MAXN], int n ) {  
  2.         int  i, j, k;  
  3.         ULL  sum;  
  4.         for( i = 0; i < n; ++i ) {  
  5.                 for( j = 0; j < n; ++j ) {  
  6.                         sum = 0;  
  7.                         for( k = 0; k < n; ++k ) sum = ( sum + (ULL)a[i][k] * (ULL)b[k][j] ) % P;  
  8.                         c[i][j] = sum;  
  9.                 }  
  10.         }  
  11. }  

因为结果需要取模,所以为了节省内存,我用了int数组存储矩阵(如果用long long,内存增加一倍)。

我脑海中就是模拟笔算,每次确定一个c的一个位置,然后用a对应的行和b对应的列去累加乘积。

用一个local variable sum来存储和,最后赋值给c[i][j],这样sum应该会被优化成一个寄存器。

代码的问题是:1 每次计算都要取模,而且是在最内层循环,是程序运算量最大的地方;2 对b的取值是按列取的,增大了cpu cache的miss率,我们都知道,按照顺序读取内存是最有效率的。

我想,也许数据没有那么强,可以取个巧,累计时不取模(假设sum一直不会溢出),最后才取模,这样取模操作就由O(n^3),降为O(n^2)。

结果得到了一个WA,也就是说,数据累加和超过了unsigned long long的最大范围(2^64-1,大约是18*10^18)代码如下:

  1. void  mat_mul( int (*a)[MAXN], int (*b)[MAXN], int (*c)[MAXN], int n ) {  
  2.         int  i, j, k;  
  3.         ULL  sum;  
  4.         for( i = 0; i < n; ++i ) {  
  5.                 for( j = 0; j < n; ++j ) {  
  6.                         sum = 0;  
  7.                         for( k = 0; k < n; ++k ) sum += (ULL)a[i][k] * (ULL)b[k][j];  
  8.                         c[i][j] = sum % P;  
  9.                 }  
  10.         }  
  11. }  

接下去我又做了一些尝试,包括:1 输出部分之前有个if判断,将这个分支拆开;2 尝试将unsigned long long换成long long;3 和排名靠前的网友对比代码(比我快100ms左右),暂时只发现他是用long long存储矩阵的。

结果时间没有变化,甚至3还导致我的内存增大一倍。我想哭了,同样都是3重循环,做人的差距咋就这么大捏?

于是我又认真的研究了锋巅网友的代码,为啥就比我快,终于让我发现了,原来他循环的顺序和我有区别。

照着修改得到了一个828ms的代码:

  1. void  mat_mul( int (*a)[MAXN], int (*b)[MAXN], int (*c)[MAXN], int n ) {  
  2.         int  i, j, k, *p1, *p2, *end;  
  3.         ULL  tmp;  
  4.         memset( c, 0, sizeof( A[0] ) );  
  5.         for( i = 0; i < n; ++i ) {  
  6.                 for( k = 0; k < n; ++k ) {  
  7.                         tmp = a[i][k];  
  8.                         for( p1 = c[i], p2 = b[k], end = p1 + n; p1 != end; ++p1, ++p2 )  
  9.                                 *p1 = ( *p1 + tmp * (*p2) ) % P;  // c[i][j] = ( c[i][j] + a[i][k] * b[k][j] ) % P;  
  10.                 }  
  11.         }  
  12. }  

这份代码将k循环移动到了中间,最内层变成了j循环(我用指针改写了,含义就是注释的那句,效率应该不会比二维引用的效率差,这个有待确定)。

这个代码的意义在于:在最内层循环,对于c和b的访问都是顺序的了,而这个循环中a[i][k]不变,这样就更好的利用了cpu cache。矩阵越大,这个加速效果越明显。

以后的实际工程,应该也会用到这个思路。

最后就是对取模的优化,既然全部累加不行,那我就部分累加,然后取一次模,这样终究可以减少取模这种最耗时的操作。

分析数据,假设a和b矩阵的数据都接近最大可能值(对于P取模,最大值是P-1,P的值大约是10^9),那么一次乘积就是10^18,那么一个unsigned long long大约可以放18个累加。我取了16个,每累加16次(取16一方面是因为已经比较接近18了,另一方面是可以很好地利用位操作),取一次模,这样取模次数大约变成原来的1/16,当然判断16这个次数又增加了分支,不过这个相对于取模的优化,已经几乎可以忽略了。耗时484ms,代码如下(有点ugly):

  1. void  mat_mul( int  (*a)[MAXN], int (*b)[MAXN], int  (*c)[MAXN], int n ) {  
  2.         int  i, j, k, L, *p2;  
  3.         ULL  tmp[MAXN], con;  
  4.         //memset( c, 0, sizeof( A[0] ) );  
  5.         for( i = 0; i < n; ++i ) {  
  6.                 memset( tmp, 0, sizeof( tmp ) );  
  7.                 for( k = 0, L = (n & ~15); k < L; ++k ) {  
  8.                         con = a[i][k];  
  9.                         for( j = 0, p2 = b[k]; j < n; ++j, ++p2 )  
  10.                                 tmp[j] += con * (*p2);  
  11.                         if( ( k & 15 ) == 15 ) {  
  12.                                 for( j = 0; j < n; ++j ) tmp[j] %= P;  
  13.                         }  
  14.                 }  
  15.                   
  16.                 for( ; k < n; ++k ) {  
  17.                         con = a[i][k];  
  18.                         for( j = 0, p2 = b[k]; j < n; ++j, ++p2 )  
  19.                                 tmp[j] += con * (*p2);  
  20.                 }  
  21.                  for( j = 0; j < n; ++j )  
  22.                         c[i][j] = tmp[j] % P;  
  23.         }  
  24. }  

代码变长了,L是16的整倍数,后面的循环是处理不足16剩下的。用了tmp数组来优化(按我的理解,栈变量会比全局变量的访问更快)。

当然,这个题还可以优化IO,因为输入输出量很大,但是这样带来的速度提升意义不大,就没再修改。

本来以为不用写了,没想到刚才又做了一个优化,竟然达到了265ms,既然如此,还是再写一下吧。。。

代码如下:

  1. void  mat_mul( int  (*a)[MAXN], int (*b)[MAXN], int  (*c)[MAXN], int n ) {  
  2.         int  i, j, k, L, *p2;  
  3.         ULL  tmp[MAXN], con;  
  4.         //memset( c, 0, sizeof( A[0] ) );  
  5.         for( i = 0; i < n; ++i ) {  
  6.                 memset( tmp, 0, sizeof( tmp ) );  
  7.                 for( k = 0, L = (n & ~15); k < L; ) {  
  8.   
  9. #define  OP  do { for( con = a[i][k], p2 = b[k], j = 0; j < n; ++j, ++p2 ) \  
  10.                         tmp[j] += con * (*p2); \  
  11.                     ++k; } while(0)  
  12.                         OP; OP; OP; OP;  
  13.                         OP; OP; OP; OP;  
  14.                         OP; OP; OP; OP;  
  15.                         OP; OP; OP; OP;  
  16.                           
  17.                         for( j = 0; j < n; ++j ) tmp[j] %= P;  
  18.                 }  
  19.                   
  20.                 for( ; k < n; ) {  
  21.                         OP;  
  22.                 }  
  23.                  for( j = 0; j < n; ++j )  
  24.                         c[i][j] = tmp[j] % P;  
  25.         }  
  26. }  

这个代码相当于去掉了分支预测,手动将16个操作展开,没想到效果这么明显。

时间: 2024-05-16 11:28:54

矩阵乘法cache优化的相关文章

OpenBLAS项目与矩阵乘法优化 | AI 研习社

提起矩阵计算,学过<高等数学>的人可能都听过,但若不是这个领域的研究者,恐怕也只停在"听过"的程度.在矩阵计算领域,开源项目OpenBLAS影响巨大,除IBM.华为等巨头公司在使用外,还吸引了全球的研究院校.开发者们关注. 雷锋网 AI 研习社近日有幸邀请到了澎峰科技创始人.OpenBLAS项目创始人和主要维护者张先轶,他将为我们介绍OpenBLAS开源项目以及矩阵乘法的优化. 嘉宾介绍 张先轶,中国科学院博士,MIT博士后,OpenBLAS开源项目创始人和主要维护者,Pe

【算法导论】矩阵乘法

离过年都不到十天了,还要等到这周五才能回家,想想也一年没回家了.从寒假开始到现在,已经有二十来天,这期间把2014年总结中的寒假计划也大多数完成了:The Element Of Style的阅读,三门数学课<随机过程>.<工程优化>.<数值分析>的算法实现.回家过年期间肯定不会写博客了,今天一看,这个月只写了三篇,于是乎今天必须再写一篇来完成这个月的基本工作量.言归正传,这篇文章写写选修课<算法设计>作业题中的矩阵乘法的三种方法. 矩阵乘法 传统方法 理论公

C语言科学计算入门之矩阵乘法的相关计算_C 语言

1.矩阵相乘矩阵相乘应满足的条件: (1) 矩阵A的列数必须等于矩阵B的行数,矩阵A与矩阵B才能相乘: (2) 矩阵C的行数等于矩阵A的行数,矩阵C的列数等于矩阵B的列数: (3) 矩阵C中第i行第j列的元素等于矩阵A的第i行元素与矩阵B的第j列元素对应乘积之和,即 如: 则: 2. 常用矩阵相乘算法    用A的第i行分别和B的第j列的各个元素相乘求和,求得C的第i行j列的元素,这种算法中,B的访问是按列进行访问的,代码如下: void arymul(int a[4][5], int b[5]

C中实现矩阵乘法的一种高效的方法_C 语言

如何计算矩阵乘法,这个大家都知道.通常情况下,我们都是用以下代码实现的: 复制代码 代码如下: for(i=0;i<n;++i)    for(j=0;j<n;++j){        sum=0;        for(k=0;k<n;++k)            sum+=A[i][k]*B[k][j];        C[i][j]+=sum;} 但是考虑了高速缓存的问题后,其实有一种更好的实现方式: 复制代码 代码如下: for(i=0;i<n;++i)    for(k

基于CPU访存局部性原理下的矩阵乘法实现

1 for(i=0;i<n;++i) 2 for(j=0;j<n;++j) 3 { 4 double sum=0; 5 for(k=0;k<n;++k) 6 sum+=a[i][k]*b[k][j]; 7 c[i][j]+=sum; 8 } 1 for(i=0;i<n;++i) 2 for(k=0;k<n;++k) 3 { 4 r=a[i][k]; 5 for(j=0;j<n;++j) 6 c[i][j]+=r*b[k][j]; 7 } 细看一番就会发现这两种实现语义是

Mapreduce实现矩阵乘法的算法思路

大数据计算中经常会遇到矩阵乘法计算问题,所以Mapreduce实现矩阵乘法是重要的基础知识,下文我尽量用通俗的语言描述该算法. 1.首先回顾矩阵乘法基础 矩阵A和B可以相乘的前提是,A的列数和B的行数相同,因为乘法结果的矩阵C中每一个元素Cij,是A的第i行和B的第j列做点积运算的结果,参见下图: 2.进入正题 在了解了矩阵乘法规则后,我们打算采用分布式计算模型Mapreduce来完成这一过程. MR过程是在Hadoop集群的多台机器上同时进行的,所以能MR化的计算必须是没有前后关系.相互独立的

MapReduce实现矩阵乘法:实现代码

编程环境: java version "1.7.0_40" Eclipse Kepler Windows7 x64 Ubuntu 12.04 LTS Hadoop2.2.0 Vmware 9.0.0 build-812388 输入数据: A矩阵存放地址:hdfs://singlehadoop:8020/workspace/dataguru/hadoopdev/week09/matrixmultiply/matrixA/matrixa A矩阵内容: 3 4 6 4 0 8 matrixa

python实现矩阵乘法的方法

  本文实例讲述了python实现矩阵乘法的方法.分享给大家供大家参考.具体实现方法如下: ? 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 def matrixMul(A, B): res = [[0] * len(B[0]) for i in range(len(A))] for i in range(len(A)): for j in range(len(B[0])): fo

理解矩阵乘法

大多数人在高中,或者大学低年级,都上过一门课<线性代数>.这门课其实是教矩阵. 刚学的时候,还蛮简单的,矩阵加法就是相同位置的数字加一下. 矩阵减法也类似. 矩阵乘以一个常数,就是所有位置都乘以这个数. 但是,等到矩阵乘以矩阵的时候,一切就不一样了. 这个结果是怎么算出来的? 教科书告诉你,计算规则是,第一个矩阵第一行的每个数字(2和1),各自乘以第二个矩阵第一列对应位置的数字(1和1),然后将乘积相加( 2 x 1 + 1 x 1),得到结果矩阵左上角的那个值3. 也就是说,结果矩阵第m行与