計算機系統基礎學習筆記(4)-Cache友好代碼

程序的性能指執行程序所用的時間,顯然程序的性能與程序執行時訪問指令和數據所用的時間有很大關係,而指令和數據的訪問時間與相應的 Cache 命中率、命中時間和和缺失損失有關。對於給定的計算機系統而言,命中時間和缺失損失是肯定的。所以,指令和數據的訪存時間主要由 Cache 命中率決定,而 Cache 的命中率則主要由程序的空間局部性和時間局部性決定。算法

Cache友好代碼

下面咱們來介紹如何編寫一段Cache友好代碼,一段Cache友好代碼每每運行速度較快。但咱們須要注意如下兩點:shell

  1. 儘量多的重複使用一個數據(時間侷限性)【若是咱們須要在某個任務屢次使用一個數據時,應該儘量的一次性使用完,利用了數據的局部性特色】
  2. 儘量跨距爲1的訪問數據(空間局部性)【在訪問一個數據時,應該依次的訪問數組元素,不要跳着訪問,利用了數據的空間侷限性】

當咱們訪問一個內存地址單元時會將同一塊的的數據同時從內存讀入到Cache,這樣若是咱們繼續訪問附近的數據,那它就已經位於Cache中,訪問速度就會很快。編程

矩陣相乘

咱們知道,兩個矩陣相乘,能夠經過三層循環來實現,可使用臨時變量來累加最內層的中間結果。以矩陣A,矩陣B來給你們介紹:數組

for(i=0;i<n;i++){
		for(j=0;j<n;j++){
			for(k=0;k<n;k++){
				C[i+j*n]+=A[i+k*n]*B[k+j*n];
			}
		}
	}

事實上,循環的順序對最終結果並無影響。所以能夠有6種循環順序:ijk, ikj, jik, jki, kij, kji 等。接下來咱們以math.c這樣一個簡單的C語言程序來分別瞭解這六種順序的運行時間。函數

#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <time.h>

//ijk
void multMat1(int n, float *A,float *B,float *C)
{
	int i,j,k;
	for(i=0;i<n;i++){
		for(j=0;j<n;j++)
		{
			for(k=0;k<n;k++)
			{
				C[i+j*n] += A[i+k*n]*B[k+j*n];
			}
		}
	}
}
//ikj
void multMat2(int n, float *A,float *B,float *C)
{
	int i,j,k;
	for(i=0;i<n;i++){
		for(k=0;k<n;k++)
		{
			for(j=0;j<n;j++)
			{
				C[i+j*n] += A[i+k*n]*B[k+j*n];
			}
		}
	}
}
//jik
void multMat3(int n, float *A,float *B,float *C)
{
	int i,j,k;
	for(j=0;j<n;j++){
		for(i=0;i<n;i++)
		{
			for(k=0;k<n;k++)
			{
				C[i+j*n] += A[i+k*n]*B[k+j*n];
			}
		}
	}
}
//jki
void multMat4(int n, float *A,float *B,float *C)
{
	int i,j,k;
	for(j=0;j<n;j++){
		for(k=0;k<n;k++)
		{
			for(i=0;i<n;i++)
			{
				C[i+j*n] += A[i+k*n]*B[k+j*n];
			}
		}
	}
}
//kij
void multMat5(int n, float *A,float *B,float *C)
{
	int i,j,k;
	for(k=0;k<n;k++){
		for(i=0;i<n;i++)
		{
			for(j=0;j<n;j++)
			{
				C[i+j*n] += A[i+k*n]*B[k+j*n];
			}
		}
	}
}
//kji
void multMat6(int n, float *A,float *B,float *C)
{
	int i,j,k;
	for(k=0;k<n;k++){
		for(j=0;j<n;j++)
		{
			for(i=0;i<n;i++)
			{
				C[i+j*n] += A[i+k*n]*B[k+j*n];
			}
		}
	}
}

int main(int argc, char **argv) {
	int nmax = 1024,i,n;
	
	//函數指針數組,存放6個函數指針,分別對應着按照6種不一樣的順序執行矩陣相乘的函數
	void (*orderings[])(int,float *,float *,float *)
	= {&multMat1,&multMat2,&multMat2,&multMat3,&multMat4,&multMat5,&multMat6};
	
	char *names[] = {"ijk","ikj","jik","jki","kij","kji"};
	
	//聲明瞭三個浮點類型指針變量A,B,C
	float *A = (float *)malloc(nmax*nmax * sizeof(float));
	float *B = (float *)malloc(nmax*nmax * sizeof(float));
	float *C = (float *)malloc(nmax*nmax * sizeof(float));
	
	struct timeval start,end;
	
	for(i=0;i<nmax*nmax;i++)A[i] = drand48()*2-1;
	for(i=0;i<nmax*nmax;i++)B[i] = drand48()*2-1;
	for(i=0;i<nmax*nmax;i++)C[i] = 0;
	
	for(i=0;i<6;i++)
	{
		gettimeofday(&start,NULL);
		(*orderings[i])(nmax,A,B,C);
		gettimeofday(&end,NULL);
		double seconds = (end.tv_sec - start.tv_sec)+1.0e-6 * (end.tv_usec - start.tv_usec);
		printf("%s:\tn = %d,%.3f s\n",names[i],nmax,seconds);
	}
	return 0;
}

能夠看出六種循環順序只是循環順序變化了,最內層的計算並無改變。
利用gcc命令進行編譯而且執行
性能

gcc -o0 -m32 -g math.c -o math
./math

執行結果以下:

經過執行結果能夠體現Cache算法的效率的影響是很是的大。

學習

矩陣的存儲——空間局部性

在計算機內部,矩陣中的數據按順序存儲,訪問相鄰的數據時,應當儘量地聽從空間局部性和時間局部性。其中在C語言中,二維矩陣採起地是行優先存儲,也就是依次順序地存儲每一行的數據。

接下來咱們分別討論一下在這6種順序下矩陣相乘算法的空間和時間局部性。

spa

矩陣相乘——ijk

for(i=0;i<n;i++){
		for(j=0;j<n;j++)
		{
			for(k=0;k<n;k++)
			{
				C[i+j*n] += A[i+k*n]*B[k+j*n];
			}
		}
	}


矩陣B中第j行與矩陣A中第i列中對應的元素相乘並累加,獲得矩陣C中的第j行第i列元素的值,能夠看出矩陣A的訪問不是順序的,而是跨越了一行的數據。若是矩陣過大,Cache放不下整個矩陣的數據,矩陣A的訪問就會變慢。
指針

矩陣相乘——ikj

for(i=0;i<n;i++){
		for(k=0;k<n;k++)
		{
			for(j=0;j<n;j++)
			{
				C[i+j*n] += A[i+k*n]*B[k+j*n];
			}
		}
	}


矩陣B中第k列,與矩陣A的第k行第i列的中的元素分別相乘獲得矩陣C中的第i列元素的值,能夠看到矩陣B和矩陣C的訪問都不是順序的,所以速度就最慢。
code

矩陣相乘——jik

for(j=0;j<n;j++){
		for(i=0;i<n;i++)
		{
			for(k=0;k<n;k++)
			{
				C[i+j*n] += A[i+k*n]*B[k+j*n];
			}
		}
	}


矩陣B中第j行與矩陣A的第i列中對應的元素相乘並累加獲得矩陣C中的第j行第i列元素的值。矩陣A的訪問不是順序的,而是跨越了一行,因此它的訪問速度也不快。

矩陣相乘——jki

for(j=0;j<n;j++){
		for(k=0;k<n;k++)
		{
			for(i=0;i<n;i++)
			{
				C[i+j*n] += A[i+k*n]*B[k+j*n];
			}
		}
	}


矩陣B中第j行第k列元素分別與矩陣A的第k行元素相乘,獲得矩陣C中的第j行元素的部分值。能夠看到矩陣A,矩陣C的訪問順序是順序的,對Cache的利用最好,所以jki的運行速度最快。

剩下的兩個順序,分析方法和前面四個類似,就不一一分析了。。。。。。

時間局部性

在這以前的的乘法都是考慮的是空間局部性問題,其實矩陣相乘還有時間局部性問題。以ijk順序計算爲例:

矩陣B中的一行依次與矩陣A的每一列相乘,並累加,獲得矩陣C中的一個元素值。矩陣B的時間局部性很好,每一行讀取後,與矩陣A中全部的列依次相乘,當它計算完之後,就不會再去須要使用該行的數據。可是矩陣A的時間局部性就不好,矩陣A的某一列會在不一樣的時間被屢次讀取使用。

分塊矩陣相乘

爲了解決矩陣A的時間局部性就不好的問題,咱們繼續介紹一種經常使用的算法——分塊算法,能夠部分的減緩這一個問題。

如上圖所示,咱們再也不以行和列爲單位來進行計算,將矩陣分紅一樣大小的若干塊,以塊爲單位進行計算,提高矩陣A的時間局部性。

仍然以一個簡單的C語言程序來驗證分塊策略對於程序的影響。

#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <time.h>

void multMat(int n, float *A,float *B,float *C,int blocksize)
{
	int i,j,k,b_i,b_j,b_k;
	register float tem =0;
	int block_num = n/blocksize;
	printf("block_num=%d\n",block_num);
	for(b_i=0;b_i<block_num;b_i++){
		for(b_j=0;b_j<block_num;b_j++){
			for(b_k=0;b_k<block_num;b_k++){
				for(i=0;i<blocksize;i++){
					for(j=0;j<blocksize;j++){
						tem=0;
						for(k=0;k<blocksize;k++){
							tem +=A[(b_i*blocksize+i)*n+b_k*blocksize+k]*B[(b_k*blocksize+k)*n+b_j*blocksize+j];
							C[(b_i*blocksize+i)*n+b_j*blocksize+j] += tem;
						}
					}
				}
			}
		}
	}
}
void multMat3(int n, float *A,float *B,float *C)
{
	int i,j,k;
	register float tem =0;
	for(i=0;i<n;i++){
		for(j=0;j<n;j++)
		{
			tem=0;
			for(k=0;k<n;k++){
				tem += A[i*n+k]*B[k*n+j];
			}
			C[i*n+j]=tem;
		}
	}
}

int main(int argc, char **argv) {
	int nmax = 2048,i,j;
	int blocksize=16;
	float tem = 0;
	double seconds;
	
	//聲明瞭四個2048*2048的大小的矩陣,矩陣元素爲單精度浮點數。A和B爲初始矩陣。
	float *A = (float *)malloc(nmax*nmax * sizeof(float));
	float *B = (float *)malloc(nmax*nmax * sizeof(float));
	float *C = (float *)malloc(nmax*nmax * sizeof(float));
	float *D = (float *)malloc(nmax*nmax * sizeof(float));
	
	struct timeval start,end;
	
	for(i=0;i<nmax*nmax;i++)A[i] = drand48()*2-1;//對矩陣A進行賦值
	for(i=0;i<nmax*nmax;i++)B[i] = drand48()*2-1;//對矩陣B進行賦值
	for(i=0;i<nmax*nmax;i++)C[i] = 0;//存放未分塊相乘的結果
	for(i=0;i<nmax*nmax;i++)D[i] = 0;//存放分塊相乘的結果
	
	gettimeofday(&start,NULL);
	multMat3(nmax, A, B, D);
	gettimeofday(&end,NULL);
	
	seconds = (end.tv_sec - start.tv_sec)+1.0e-6 * (end.tv_usec - start.tv_usec);
	printf("n= %d,time = %.3f s\n",nmax,seconds);
	
	gettimeofday(&start,NULL);
	multMat(nmax, A, B, C,blocksize);
	gettimeofday(&end,NULL);
	
	seconds = (end.tv_sec - start.tv_sec)+1.0e-6 * (end.tv_usec - start.tv_usec);
	printf("n= %d,blocksize = %d,time = %.3f s\n",nmax,blocksize,seconds);
	
	//統計兩個結果之差
	for(i=0;i<nmax;i++)
	{
		for(j=0;j<nmax;j++){
			tem+=(C[i*nmax+j]-D[i*nmax+j])*(C[i*nmax+j]-D[i*nmax+j]);
		}
	}
	printf("error=%.4f\n",tem);
	free(A);
	free(B);
	free(C);
	free(D);
	return 0;
}

該代碼首先聲明瞭四個2048*2048的大小的矩陣,矩陣元素爲單精度浮點數。其中A和B爲初始矩陣。矩陣C存放未分塊相乘的結果,矩陣存放分塊相乘的結果。在程序的末尾對這兩個結果的差做出了統計輸出,對比判斷結果的正確性。在具體的矩陣分塊算法實現時,利用了6層循環,分別是block上的三層循環和block內的三層循環。

運行結果能夠看出,在分塊的做用下時間耗費上,程序性能有了大幅的提高。也能夠改變block的大小,其結果也是相似的,並且在上述執行過程當中,矩陣的計算結果也是同樣的。

咱們能夠經過編程來充分利用cache性能,可是咱們須要同時考慮數據的組織形式和數據的訪問形式,此外,可使用分塊處理的方式得到空間局部性更好的方法。得到最佳Cache性能是與平臺相關的,好比,Cache的大小,Cache行大小,以及映射策略。比較通用的法則固然是工做集越小越好,訪問跨距越小越好。

前面的路佈滿荊棘,我有過遲疑,卻從未後退。一無全部,就是拼的理由!

以上就是本次給你們分享的計算機系統基礎學習筆記-Cache友好代碼,但願各位小夥伴能有所收穫,若是有問題的地方,能夠在評論區留言喲,固然以爲還不錯的小夥伴,就留下你的金贊吧,你的支持就是我前進的動力哇。

相關文章
相關標籤/搜索