# 2017q1 team6 matrix
constributed by <`henry0929016816` `vtim9907`>
## Column order problem
這是一個簡單的 sub-matrix 版本,把巨大的 array 切成小的 4 * 4 矩陣後再做運算。
```clike=
for (int i = 0; i < dst->row; i += 4) {
for (int j = 0; j < dst->col; j += 4) {
for (int k = 0; k < N; k += 4) {
for (int i2 = 0; i2 < 4; ++i2) {
for (int j2 = 0; j2 < 4; ++j2) {
for (int k2 = 0; k2 < 4; ++k2) {
PRIV(dst)->values[i + i2][j + j2] += PRIV(l)->values[i + i2][k + k2] *
PRIV(r)->values[k + k2][j + j2];
}
}
}
}
}
}
```
在每次跑最內圈的 for 時,取左陣列和右陣列的元素出來看 memory address 到底長怎樣,並把左右陣列 print 出的結果分開來看:
```
address PRIV(l)->values[i + i2][k + k2] = 0x7e2470
address PRIV(l)->values[i + i2][k + k2] = 0x7e2474
address PRIV(l)->values[i + i2][k + k2] = 0x7e2478
address PRIV(l)->values[i + i2][k + k2] = 0x7e247c
address PRIV(r)->values[k + k2][j + j2] = 0x803ffc
address PRIV(r)->values[k + k2][j + j2] = 0x80443c
address PRIV(r)->values[k + k2][j + j2] = 0x80487c
address PRIV(r)->values[k + k2][j + j2] = 0x804cbc
```
很明顯,右陣列因為是以 column order 的方式讀取,所以 memory address 不連續,沒辦法有效利用 cache。
不過因為這個版本並沒有特別用到硬體支援的技術,譬如 SEE、 AVX,所以 Data alignment 與這個版本比較沒有關係。
> 我再確定一下 @@
## data alignment
* 在講到 data alignment 之前,我們要先提到在程式語言裡,一個 data object 所擁有的特性是甚麼。一個 data object 具有兩個特性
* value
* storage location (address)
* 而 data alignment 的意思就是, data 的 address 可以公平的被 1, 2, 4, 8,這些數字整除,從這些數字可以發現他們都是 2 的 N 次方 (N為從 0 開始的整數)。換句話說,這些 data object 可以用 1 ,2 ,4 ,8 byte 去 alignment。
* 懂得 data alignment 是甚麼之後,就要開始講電腦的 cpu 如何抓取資料,cpu 不會一次只抓取 1 byte 的資料,因為這樣太慢了,如果有個資料型態是 int 的 資料,如果你只抓取 1 byte ,就必須要抓 4 次(int 為 4 byte),有夠慢。所以 cpu 通常一次會取 4 byte(要看電腦的規格 32 位元的 cpu 一次可以讀取 32 bit 的資料,64 位元一次可以讀取 64 bit),並且是按照順序取的,
例如:
* 第一次取:0~3
* 第二次取:4~7
![](https://i.imgur.com/aDCYyWc.png)
所以如果你的資料是分布在 1~4 那還是會
* 第一次取:0~3 將,0 的資料去掉,留下 1~3
* 第二次取:4~7 將,5~7 的資料去掉,留下 4
* 再將 1~3 4 合起來
![](https://i.imgur.com/wIfEVy9.png)
由於資料分布不在 4 的倍數,導致了存取速度降低,~~為了避免這個情況,我們一開始就要先做好資料的處理,連續資料的 address 都能是 4 的倍數~~而 compiler 一開始再分配記憶體時,就會按照宣告的型態去做 alignment ,例如 int 就是 4 byte alignment。
* struct 會自動做 alignment,假設創了一個 struct,如下面 code 所示
```c=
struct s1 {
char c;
int a;
}
```
照我們學過的 char 為 1 byte,而 int 為 4 byte ,兩個加起來應該為 5 byte,然而實際上為 8 byte,由於 int 為 4 byte ,所以 char 為了要能 alignment 4 byte 就多加了 3 byte 進去 ,使得 cpu 存取速度不會因 address 不是在 4 的倍數上,存取變慢。
* 原本想說 struct 會自動 alignment 那我們幹嘛討論呢? 後來實驗了一下,發現在 struct 裡放陣列他並不會自動 alignment
```c=
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
typedef struct _s1
{
char a[5];
}s1;
int main()
{
int i;
s1 p[10];
printf("struct s1 size: %ld byte\n",sizeof(s1));
for(i=0;i<10;i++)
{
printf("the struct p[%d] address =%p\n",i,p+i);
}
}
```
得到執行結果為
struct s1 size: 5 byte
the struct p[0] address =0x7ffe72bfd270
the struct p[1] address =0x7ffe72bfd275
the struct p[2] address =0x7ffe72bfd27a
the struct p[3] address =0x7ffe72bfd27f
the struct p[4] address =0x7ffe72bfd284
the struct p[5] address =0x7ffe72bfd289
the struct p[6] address =0x7ffe72bfd28e
the struct p[7] address =0x7ffe72bfd293
the struct p[8] address =0x7ffe72bfd298
the struct p[9] address =0x7ffe72bfd29d
~~所以如果我們現在探討的議題,matrix 不是 4x4 的大小,可能是 3x3,那我們就要考慮如何 aligment 才能使資料的位址符合 cpu 抓取的的方式。~~
:::warning
注意 :zap:
以上是我搞錯了,我以為只要不是對齊 4 byte 就沒有自動 alignment ,然而其實還是有 alignment 的,由於 char type 的 data 大小只佔 1 byte 所以只要 1 byte alignment ,也就是不用使用 padding 讓其變成 4 的倍數。而我們原本的 matrix 的實作也可以不用考慮到 data alignment 由於 compiler 會自動幫我們以 data 的大小做 alignment ,假設有 int type(4 byte ) 他再創建的時候 就會是已 4 byte alignment。
:::
* 現代的處理器再處理一般的讀取跟寫入資料,不用考慮 data alignment 的議題,例如 intel x86 系統 是允許 data unalignment 的以下用我的電腦實驗 data alignment 跟 data unalignment 的差異。
* 首先我創立兩個 struct 兩個放的東西是相同的,唯一不同的是 t1 有加 pack 這條指令告訴 compiler 說 test1 裡的 data 只要 1 byte alignment 就好,t2 則是會按照宣告的 type 作 alignment 所以 t2 裡會有 padding。
:::warning
注意 :zap:
以下實驗為不好的實驗,struct 裡的資料量太小,可能看不出兩個 struct 執行時間的差異,另外沒有去清除 cache 導致讀取的資料可能會再 cache 裡而使得處理速度一樣快的錯覺
```c=
...
...
#pragma pack(push)
#pragma pack(1)
typedef struct _test1
{
int t;
char a;
int b;
}test1;
#pragma pack(pop)
typedef struct _test2
{
int t;
char a;
int b;
}test2;
```
分別讓兩個 struct 執行對 b 的資料操作
```
...
for(int i=0;i<4000;i++)
{
t1.b=0;
t1.b+=1;
}
...
...
for(int i=0;i<4000;i++)
{
t2.b=0;
t2.b+=1;
}
...
```
發現執行的時間沒有差異
* 執行結果
struct test1 size:9
struct test2 size:12
alignment excution time : 0.000011(s)
unalignment excution time : 0.000012(s)
證明了現代處理器其實不用考慮 data alignment 議題,然而老舊的處理器可能就不是這樣了(手邊沒有可以實驗的)
:::
:::info
重新設計實驗
* 方式一:將 struct 資料結構改大
```c=
#pragma pack(1)
typedef struct _test1
{
char c[3];
int num[256];
}test1;
#pragma pack(pop)
typedef struct _test2
{
char c[3];
int num[256];
}test2;
```
* 方式二:去掉 cache 的影響
在 Makefile 裡加入這一條指令
`echo 3 > /proc/sys/vm/drop_caches`
並且讓這段 code 跑個 200 次 看是否平均起來的執行速度差不多
```c=
for i in `seq 0 1 200`; do \
echo 3 > /proc/sys/vm/drop_caches ;\
printf "%d," $$i;\
./pointer; \
done > clock_gettime.txt
```
* 最後畫出圖形
![](https://i.imgur.com/yUS7zcw.png)
有幾比測資誤差滿大的,但大部份都相同,關於誤差很大的問題,到現在還是不懂,只知道這個問題從第1週就一直困惑我滿久的,知道跟 cache 有關 但不知道為什麼會這個樣子。
:::
* 要考慮 data alignment 議題的只有當我們要實作 avx 板的矩陣乘法,由於 avx 操作的資料量比較大(32 byte),所以我們要幫資料對齊 32 byte,當然不對齊也是可以操作,只是會比較慢,以下我會實驗看看 32 byte alignment 跟 unalignment 的差異。
>[color=blue]今天剛好有看到 c11 有提供一個新的函式 叫 void *aligned_alloc(size_t alignment, size_t size); 可以自訂要 align 的 size,等晚點再來看的詳細說明)
## 矩陣乘法 SSE 版
因為 SSE 的暫存器 (XMM0 ~ XMM7) 有 128 bits 的長度,也就是 4 個 integer 的大小,根據這個特性,在做矩陣乘法時,選擇將大矩陣切割成多個 4 * 4 的小矩陣來做會比較有效率。
### data alignment
因為原本要做乘法的兩個矩陣都是用 malloc 配置出來的,在 [Matrix Multiplication using SIMD](https://hackmd.io/s/Hk-llHEyx#對齊下的-sse) 有看到 malloc 本來配置出來的記憶體位置就有做 alignment,根據 malloc 的 man page 裡提到 :
```
The malloc() and calloc() functions return a pointer to the allocated memory, which is
suitably aligned for any built-in type.
```
但單看這句英文,其實我意會不太過來實際上到底 malloc 做了怎樣的 data alignment,所以又查了 [The GNU C Library - Malloc Example](ftp://ftp.gnu.org/old-gnu/Manuals/glibc-2.2.3/html_node/libc_25.html),裡面有特別提到 :
```
In the GNU system, the address is always a multiple of eight on most systems, and a
multiple of 16 on 64-bit systems.
```
這句我就有看懂,所以說在 "大多數" 系統下,malloc 會以 8 bytes 進行對齊;而在 64-bit 的系統下,malloc 則會以 16 bytes 進行對齊。
做了小實驗,malloc 在我的系統(64-bit)下是以 16 bytes 對齊:
```clike=
for(int i=0;i<10000;++i){
char *z;
z=malloc(sizeof(char));
}
```
結果用 gdb 測過之後,發現位址的結尾的確都是 0 結尾,表示真的是以 16-byte 做對齊。
>[color=red]其實這邊最大的疑惑是在 [alignment 系列 function 的 man page](http://man7.org/linux/man-pages/man3/posix_memalign.3.html) 裡,最下面的 NOTES 提到 " The glibc malloc(3) always returns 8-byte aligned memory addresses ",所以才不太確定到底是只會對齊 8-byte,才去查其他文獻和做實驗,證明 malloc 在 64-bit 系統下會以 16-byte 對齊,所以也許上面貼的 man page 是單指 32-bit 的系統吧!?
確認了 malloc 的機制後,可以確定矩陣不需要額外用其他 function 做 data alignment,用原本 malloc 的配置方式,就可以直接按原計畫做 SSE 版本的矩陣乘法,就直接實作。
### 先做矩陣轉置 ?
原本有在思考要不要先把第二個矩陣先做轉置,這樣就不會有 column major 的問題,不過想想,這樣有點太大費周章...,把轉置的時間也算進去的話,效能大概也會變慢,還是算了@@
### sse_matrix
實做的部份我把 4 * 4 的小矩陣乘法做了 loop unrolling,所以 code 有一點點長,不過因為想到一種比較特別的矩陣乘法實做方式,滿精簡的,所以實際用到的指令其實比我預想的少。
code :
```clike=
for (int i = 0; i < dst->row; i += 4) {
for (int j = 0; j < dst->col; j += 4) {
__m128i dst0 = _mm_setzero_si128();
__m128i dst1 = _mm_setzero_si128();
__m128i dst2 = _mm_setzero_si128();
__m128i dst3 = _mm_setzero_si128();
for (int k = 0; k < l->col; k += 4) {
__m128i r1 = _mm_load_si128((__m128i *)(&PRIV(r)->values[k][j]));
__m128i r2 = _mm_load_si128((__m128i *)(&PRIV(r)->values[k+1][j]));
__m128i r3 = _mm_load_si128((__m128i *)(&PRIV(r)->values[k+2][j]));
__m128i r4 = _mm_load_si128((__m128i *)(&PRIV(r)->values[k+3][j]));
__m128i b1 = _mm_set1_epi32(PRIV(l)->values[i][k]);
__m128i b2 = _mm_set1_epi32(PRIV(l)->values[i][k+1]);
__m128i b3 = _mm_set1_epi32(PRIV(l)->values[i][k+2]);
__m128i b4 = _mm_set1_epi32(PRIV(l)->values[i][k+3]);
__m128i b5 = _mm_set1_epi32(PRIV(l)->values[i+1][k]);
__m128i b6 = _mm_set1_epi32(PRIV(l)->values[i+1][k+1]);
__m128i b7 = _mm_set1_epi32(PRIV(l)->values[i+1][k+2]);
__m128i b8 = _mm_set1_epi32(PRIV(l)->values[i+1][k+3]);
__m128i b9 = _mm_set1_epi32(PRIV(l)->values[i+2][k]);
__m128i b10 = _mm_set1_epi32(PRIV(l)->values[i+2][k+1]);
__m128i b11 = _mm_set1_epi32(PRIV(l)->values[i+2][k+2]);
__m128i b12 = _mm_set1_epi32(PRIV(l)->values[i+2][k+3]);
__m128i b13 = _mm_set1_epi32(PRIV(l)->values[i+3][k]);
__m128i b14 = _mm_set1_epi32(PRIV(l)->values[i+3][k+1]);
__m128i b15 = _mm_set1_epi32(PRIV(l)->values[i+3][k+2]);
__m128i b16 = _mm_set1_epi32(PRIV(l)->values[i+3][k+3]);
__m128i t0 = _mm_add_epi32(
_mm_add_epi32(
_mm_mullo_epi32(b1, r1),
_mm_mullo_epi32(b2, r2)),
_mm_add_epi32(
_mm_mullo_epi32(b3, r3),
_mm_mullo_epi32(b4, r4)));
__m128i t1 = _mm_add_epi32(
_mm_add_epi32(
_mm_mullo_epi32(b5, r1),
_mm_mullo_epi32(b6, r2)),
_mm_add_epi32(
_mm_mullo_epi32(b7, r3),
_mm_mullo_epi32(b8, r4)));
__m128i t2 = _mm_add_epi32(
_mm_add_epi32(
_mm_mullo_epi32(b9, r1),
_mm_mullo_epi32(b10, r2)),
_mm_add_epi32(
_mm_mullo_epi32(b11, r3),
_mm_mullo_epi32(b12, r4)));
__m128i t3 = _mm_add_epi32(
_mm_add_epi32(
_mm_mullo_epi32(b13, r1),
_mm_mullo_epi32(b14, r2)),
_mm_add_epi32(
_mm_mullo_epi32(b15, r3),
_mm_mullo_epi32(b16, r4)));
dst0 = _mm_add_epi32(t0, dst0);
dst1 = _mm_add_epi32(t1, dst1);
dst2 = _mm_add_epi32(t2, dst2);
dst3 = _mm_add_epi32(t3, dst3);
}
_mm_store_si128((__m128i *)(&PRIV(dst)->values[i][j]), dst0);
_mm_store_si128((__m128i *)(&PRIV(dst)->values[i+1][j]), dst1);
_mm_store_si128((__m128i *)(&PRIV(dst)->values[i+2][j]), dst2);
_mm_store_si128((__m128i *)(&PRIV(dst)->values[i+3][j]), dst3);
}
}
```
過程簡單講大概如下:
假設矩陣 A x B = C
$\begin{bmatrix}
A_{1} & A_{2} & A_{3} & A_{4} \\
A_{5} & A_{6} & A_{7} & A_{8} \\
A_{9} & A_{10} & A_{11} & A_{12} \\
A_{13} & A_{14} & A_{15} & A_{16} \\
\end{bmatrix}$ X $\begin{bmatrix}
B_{1} & B_{2} & B_{3} & B_{4} \\
B_{5} & B_{6} & B_{7} & B_{8} \\
B_{9} & B_{10} & B_{11} & B_{12} \\
B_{13} & B_{14} & B_{15} & B_{16} \\
\end{bmatrix}$ = $\begin{bmatrix}
C_{1} & C_{2} & C_{3} & C_{4} \\
C_{5} & C_{6} & C_{7} & C_{8} \\
C_{9} & C_{10} & C_{11} & C_{12} \\
C_{13} & C_{14} & C_{15} & C_{16} \\
\end{bmatrix}$
分割步驟來看,先算 $C_1$ ~ $C_4$ :
* 使用 _mm_set1_epi32 可以塞四個相同的 int 到一個 __m128i 裡,如下
$D_1$ = $\begin{bmatrix} A_{1} & A_{1} & A_{1} & A_{1} \end{bmatrix}$
$D_2$ = $\begin{bmatrix} A_{2} & A_{2} & A_{2} & A_{2} \end{bmatrix}$
$D_3$ = $\begin{bmatrix} A_{3} & A_{3} & A_{3} & A_{3} \end{bmatrix}$
$D_4$ = $\begin{bmatrix} A_{4} & A_{4} & A_{4} & A_{4} \end{bmatrix}$
* 而 $R$ 就先存 B 的列
$R_1$ = $\begin{bmatrix} B_{1} & B_{2} & B_{3} & B_{4} \end{bmatrix}$
$R_2$ = $\begin{bmatrix} B_{5} & B_{6} & B_{7} & B_{8} \end{bmatrix}$
$R_3$ = $\begin{bmatrix} B_{9} & B_{10} & B_{11} & B_{12} \end{bmatrix}$
$R_4$ = $\begin{bmatrix} B_{13} & B_{14} & B_{15} & B_{16} \end{bmatrix}$
* 然後就可以用 _mm_mullo_epi32 做相乘,然後全部加起來,就會得到 $C_1$ ~ $C_4$
_mm_mullo_epi32($R_1$, $D_1$)+
_mm_mullo_epi32($R_2$, $D_2$)+
_mm_mullo_epi32($R_3$, $D_3$)+
_mm_mullo_epi32($R_4$, $D_4$)
= $C_1$ ~ $C_4$
用同樣概念做四次,就完成了一次 4 * 4 的小矩陣相乘!
* 效能
* submatrix 版本
![](https://i.imgur.com/CweT6gq.png)
* sse 版本
![](https://i.imgur.com/9BnvZMK.png)
## 矩陣乘法 AVX 版
### avx set 版乘法
```c=
matrix avx_multiple(matrix a,matrix b)
{
matrix result;
__m256 r[2][4];
for(int i=0;i<4;i++) {
__m256 a_row_01=_mm256_set_ps(a.value[0][i],a.value[0][i],a.value[0][i],a.value[0][i],
a.value[1][i],a.value[1][i],a.value[1][i],a.value[1][i]);
__m256 a_row_23=_mm256_set_ps(a.value[2][i],a.value[2][i],a.value[2][i],a.value[2][i],
a.value[3][i],a.value[3][i],a.value[3][i],a.value[3][i]);
__m256 b_col=_mm256_set_ps(b.value[i][0],b.value[i][1],b.value[i][2],b.value[i][3],
b.value[i][0],b.value[i][1],b.value[i][2],b.value[i][3]);
r[0][i]=_mm256_mul_ps (a_row_01,b_col);
r[1][i]=_mm256_mul_ps (a_row_23,b_col);
}
__m256 sum0= _mm256_add_ps (r[0][0],r[0][1]);
sum0 = _mm256_add_ps (sum0,r[0][2]);
sum0 = _mm256_add_ps (sum0,r[0][3]);
__m256 sum1= _mm256_add_ps (r[1][0],r[1][1]);
sum1 = _mm256_add_ps (sum1,r[1][2]);
sum1 = _mm256_add_ps (sum1,r[1][3]);
result.value[0][0]=sum0[7];
result.value[0][1]=sum0[6];
result.value[0][2]=sum0[5];
result.value[0][3]=sum0[4];
result.value[1][0]=sum0[3];
result.value[1][1]=sum0[2];
result.value[1][2]=sum0[1];
result.value[1][3]=sum0[0];
result.value[2][0]=sum1[7];
result.value[2][1]=sum1[6];
result.value[2][2]=sum1[5];
result.value[2][3]=sum1[4];
result.value[3][0]=sum1[3];
result.value[3][1]=sum1[2];
result.value[3][2]=sum1[1];
result.value[3][3]=sum1[0];
return result;
}
```
* 執行時間跟 naive 板相比
avx execution time 0.000579(s)
naive execution time 0.001123(s)
可以發現有比 naive 版本的乘法還要快
* 首先,先來介紹 code 的 概念,我們都知道 matrix 乘法是行列乘法
$\begin{bmatrix}
A_{1} & A_{2} \\
A_{3} & A_{4} \end{bmatrix}$X$\begin{bmatrix}
B_{1} & B_{2} \\
B_{3} & B_{4} \end{bmatrix}$=$\begin{bmatrix}
A_{1}B_{1}+A_{2}B_{3} & A_{1}B_{2}+A_{2}B_{4} \\
A_{3}B_{1}+A_{4}B_{3} & A_{3}B_{2}+A_{4}B_{4} \end{bmatrix}$
而我的寫法就是分別拆開相乘得到的結果,再相加得到答案
$\begin{bmatrix} A_{1}B_{1} & A_{1}B_{2} \\
A_{3}B_{1} & A_{3}B_{2} \end{bmatrix}+\begin{bmatrix}
A_{2}B_{3} & A_{2}B_{4} \\
A_{4}B_{3} & A_{4}B_{4} \end{bmatrix}=\begin{bmatrix}
A_{1}B_{1}+A_{2}B_{3} & A_{1}B_{2}+A_{2}B_{4} \\
A_{3}B_{1}+A_{4}B_{3} & A_{3}B_{2}+A_{4}B_{4} \end{bmatrix}$
* `_mm256_set_ps (float e7, float e6, float e5, float e4, float e3, float e2, float e1, float e0)` 是設定 avx 的 register 裡的值要為多少的函式,由於我們是 float type 的 matrix 所以要將 8 個值傳進去 ,傳值的方式還是一個一個傳,而這種作法很慢, 如果能一次就讀取 8 個值想必會快的多, 而這樣就要使用 _mm256_load_ps 這個函式 。
* `_mm256_load_ps (float const * mem_addr)` 由參數的 address 開始往後讀取 31 byte 的資料,如果傳入的 address 不是 32 byte alignment 會出錯 (core dumped), 由於讀取到錯誤的記憶體,所以這裡就要考慮到 data alignment 的議題了(終於啊!)
>[name=henry0929016816]關於這裡我還是有點不懂為何會讀取到錯誤的記憶體
## memory leak
## 參考資料
[Data Alignment](http://www.songho.ca/misc/alignment/dataalign.html)
[Alignment in C](https://wr.informatik.uni-hamburg.de/_media/teaching/wintersemester_2013_2014/epc-14-haase-svenhendrik-alignmentinc-paper.pdf)
[pragma](http://topalan.pixnet.net/blog/post/22334686)