# 2018q3 矩陣乘法 Matrix multiplication
Contributed by < [DyslexiaS](https://github.com/DyslexiaS), [siahuat0727](https://github.com/siahuat0727), [pjchiou](https://github.com/pjchiou) >
[GitHub](https://github.com/siahuat0727/matrix-multiplication)
Task : 重現 [Matrix Multiplication using SIMD](https://hackmd.io/s/Hk-llHEyx) 實驗,並依循 CS:APP 第 6 章指引,分析個別實作的效能
---
## outline
- 演算法
- SSE 指令集與 AVX 指令集的對齊問題。
## 演算法
- [x] naive
- [x] cache-friendly
- [x] sub-matrix
- [x] strassen
- [x] SIMD-SSE
- [x] SIMD-AVX
---
### Naive algorithm
最簡單的矩陣相乘演算法
- time complexity : $O(n^3)$
- 演算法
```click=1
for (int i = 0; i < a.row; ++i)
for (int j = 0; j < b.col; ++j) {
int sum = 0;
for (int k = 0; k < a.col; ++k)
sum += a.values[i][k] * b.values[k][j];
dst->values[i][j] += sum;
}
```
---
### cache-friendly
在 naive algorithm 中, `b.values[k][j]` 一直在取用距離為 `b.col` 的變數,這樣對 cache 的使用不利。要利用 cache 的特性來加速運算,把握兩個重點
1. Temporal locality : 一旦取用過的變數,重複取用。
2. Spatial locality : 取用記憶體中相鄰的變數。
- time complexity : $O(n^3)$
- 演算法
```clike=1
for (int i = 0; i < a.row; ++i)
for (int k = 0; k < a.col; ++k) {
int r = a.values[i][k];
for (int j = 0; j < b.col; ++j)
dst->values[i][j] += r * b.values[k][j];
}
```
如此一來,重複取用 `a[i][k] `且每個矩陣都是以步長為 1 取用變數。
---
### Sub-matrix
Divied and conquer 的概念,先把矩陣拆成較小的矩陣,各自運算後再加起來。
![](https://i.imgur.com/VlBLSXb.png)
在這個示意圖中,`A`、`B`兩個矩陣都是正方形的,其 sub-matrix 行列皆為原本的一半。這與 naive 版本的功能不符,所以我們將其稍做調整
1. `A`、`B` 矩陣不必為正方形,符合矩陣相乘的限制 `A.col==B.row` 即可。
2. sub-matrix 行列大小不必為 N/2 ,但必須為正方形。
3. sub-matrix 大小亦不必可被 C.row 或 C.col 整除。
- time complexity : $O(n^3)$
- 演算法
- reference : [divide and conquer](https://www.geeksforgeeks.org/strassens-matrix-multiplication/)
```clike=1
//Calculate sub-matrix at (c_row,c_col) and the size is stride.
void matmul_stride(const Matrix l,
const Matrix r,
const Matrix dst,
int c_row,
int c_col,
int stride)
{
for (int k = 0; k < l.col; k += stride)
for (int i = 0; i < stride && i + c_row < l.row; i++)
for (int j = 0; j < stride && j + c_col < r.col; j++)
for (int m = k; m < k + stride && m < l.col; m++)
dst.values[i + c_row][j + c_col] +=
l.values[i + c_row][m] * r.values[m][j + c_col];
}
void sub_matmul(const Matrix l, const Matrix r, const Matrix dst, void *ctx)
{
CHECK(l, r, dst);
assert(ctx != NULL);
int stride = ((SubMatrixInfo*)ctx)->stride;
for (int i = 0; i < l.row; i += stride)
for (int j = 0; j < r.col; j += stride)
matmul_stride(l, r, dst, i, j, stride);
}
```
---
### Strassen
兩個矩陣相乘所要用到的乘法及加法次數:
因為 $n \times n$ 矩陣 $C$ 有 $n^2$ 個元素,計算每一元素需要 n 個乘法和 (n-1) 個加法,故知 $n\times n$ 階矩陣乘積共使用了 $n^3$ 個乘法和 $n^3-n^2$ 個加法。
這個迷思直到更快速的 divide-and-conquer 矩陣乘法 strassen 被提出才打破。
- 考慮 $2\times 2$ 階分塊矩陣乘法
![](https://i.imgur.com/vN6s5iN.png)
其中每一分塊皆為方陣。傳統的矩陣乘法運算方式,$C_{ij}=A_{i1}B_{1j}+A_{i2}B_{2j}$,總共使用8個分塊乘法和4個分塊加法。
Strassen 演算法使用7個分塊乘法和18個分塊加法,如下:
\begin{aligned} P_1&=(A_{11}+A_{22})(B_{11}+B_{22})\\ P_2&=(A_{21}+A_{22})B_{11}\\ P_3&=A_{11}(B_{12}-B_{22})\\ P_4&=A_{22}(B_{21}-B_{11})\\ P_5&=(A_{11}+A_{12})B_{22}\\ P_6&=(A_{21}-A_{11})(B_{11}+B_{12})\\ P_7&=(A_{12}-A_{22})(B_{21}+B_{22})\\ C_{11}&=P_1+P_4-P_5+P_7\\ C_{12}&=P_3+P_5\\ C_{21}&=P_2+P_4\\ C_{22}&=P_1+P_3-P_2+P_6. \end{aligned}
- 直接代入化簡即可驗證上述公式的正確性
假設 $n=2m$,上面所有分塊皆為 $m\times m$ 。
- 傳統的矩陣乘法 :
- 使用了 $8m^3$ 個乘法和 $8m^3-4m^2$ 個加法。
- Strassen :
- Strassen 演算法僅執行至分塊等級,也就是說,$m\times m$ 分塊乘法仍採用傳統方法計算
- 則7次分塊乘法共使用 $7m^3$ 個乘法和 $7(m^3-m^2)$ 個加法,再加上 18 次分塊加法使用的 $18m^2$ 個加法,總計是 $7m^3$ 個乘法和 $7m^3+11m^2$ 個加法
- 若 $m\gg 1$,不論乘法或加法,Strassen 演算法僅耗費傳統方法 $7/8$ 的計算量。
Reference : [strassen](https://ccjou.wordpress.com/2013/06/04/分治矩陣乘法──strassen-演算法/)
---
### SIMD-SSE
- SIMD 為 ==Single Instruction Multiple Data== 的縮寫,以 Intel 的處理器為例,其發展的 MMX SIMD 指令集,能以一道指令處理多個資料,其目的就是為了提高多媒體資料的處理能力。其後,Intel 又發展出 SSE SIMD 指令集,為 MMX 的擴充指令集。
- AMD 的處理器亦有其 SIMD 指令集,但我用的是 Intel 的處理器,所以我以 Intel 的 `SSE SIMD` 指令集做為例子。
- SSE 有 8 個 128 bit 的暫存器,每個暫存器可以用來存
- 4 個 32 bit 的 float
- 2 個 64 bit 的 double
- 4 個 32 bit 的 integer
- 8 個 16 bit 的 short
- 16 個 8 bit 的 char
利用 SIMD 指令集做矩陣乘法時,其流程與 sub-matrix 無異,但有一個地方要注意的是,矩陣乘法的本質就是向量的內積,因為我們的暫存器大小是 8 個 128 bit ,剛好可以用來==存 8 個有 4 個 integer 的向量==,所以 sub-matrix 的大小就是 $4*4$ ,這樣可以充分利用暫存器。
我們分別用以下 4 個 vector 來存 A、B 兩個矩陣中的 sub-matrix 。
![](https://i.imgur.com/EQTRW6x.png)
我們要做的事情就是從 [SIMD 指令集](https://software.intel.com/sites/landingpage/IntrinsicsGuide/)中組合出可以達到需求的指令。以下說明如何得到最上面的 row ,其他以此類推。
==使用前注意每個 function 必須 include 哪個 header file==
1. 讀取矩陣值,存入暫存器。宣告型別為 `__128i` 的變數來代表這個 128 bit 長可存四個 32 bit integer 的變數。用以下兩個 function 來讀取矩陣值。
- __m128i _mm_load_si128 (__m128i const* mem_addr)
- __m128i _mm_set_epi32 (int e3, int e2, int e1, int e0)
因為 A 矩陣中的向量值是存在連續的記憶體內,用前者; B 矩陣用後者。
2. 將 V1 分別對 V5~V8 做內積,可得四個內積後的向量 `V15`, `V16`, `V17`, `V18`
- __m128i _mm_mullo_epi32 (__m128i a, __m128i b)。
![](https://i.imgur.com/PTJG78o.gif)
3. 我們要將這四個向量 V15, V16, V17, V18 組成的矩陣做轉置,但這四個向量是分別獨立的四個 __m128i 變數,只是在我們腦海裡是一個矩陣,我們要利用 SIMD 的四個指令達到等價效果。
- __m128i _mm_unpackhi_epi32 (__m128i a, __m128i b)
```
將兩個 m128i 變數的前兩筆 32 bits 資料交錯擺放。
r[0] = a[0] ; r[1] = b[0];
r[2] = a[1] ; r[3] = b[1];
```
- __m128i _mm_unpackhi_epi64 (__m128i a, __m128i b)
```
將兩個 m128i 變數的前兩筆 64 bits 資料交錯擺放。
r[0] = a[0]; r[1] = a[1];
r[2] = b[0]; r[3] = b[1];
```
- __m128i _mm_unpacklo_epi32 (__m128i a, __m128i b)
```
將兩個 m128i 變數的後兩筆 32 bits 資料交錯擺放。
r[0] = a[2] ; r[1] = b[2];
r[2] = a[3] ; r[3] = b[3];
```
- __m128i _mm_unpacklo_epi64 (__m128i a, __m128i b)
```
將兩個 m128i 變數的後兩筆 64 bits 資料交錯擺放。
r[0] = a[2]; r[1] = a[3];
r[2] = b[2]; r[3] = b[3];
```
先將這四個向量分為兩個一組後,依序做以下四個函式,可以得到另外四個向量。
- S1 = _mm_unpacklo_epi32(V15,V16)
- S2 = _mm_unpackhi_epi32(V15,V16)
- S3 = _mm_unpacklo_epi32(V17,V18)
- S4 = _mm_unpackhi_epi32(V17,V18)
![](https://i.imgur.com/rcXrkuc.gif)
接著再做四個函式,會得到等價於轉置的結果
- D1 = _mm_unpacklo_epi64(S1,S3)
- D2 = _mm_unpackhi_epi64(S1,S3)
- D3 = _mm_unpacklo_epi64(S2,S4)
- D4 = _mm_unpackhi_epi64(S2,S4)
![](https://i.imgur.com/uV7c4MH.gif)
4. 將這四個向量相加後,回到 (2.) 改以 V2 與 V5~V8 做內積,以此類推,最終就能得到矩陣乘積的結果。
此處完整的程式碼如下:
```clike=1
void SIMD_matmul(const Matrix l, const Matrix r, const Matrix dst, void *ctx)
{
for (int i = 0; i < l.row; i += 4)
for (int j = 0; j < r.col; j += 4)
SIMD_matmul4(l, r, dst, i, j);
}
void SIMD_matmul4(const Matrix l,
const Matrix r,
const Matrix dst,
int c_row,
int c_col)
{
__m128i I[4], R[4], D[4], T[4], S[4], Sum[4];
for (int i = 0; i < 4; i++)
Sum[i] = _mm_setzero_si128();
for (int k = 0; k < l.col; k += 4) {
// Read matrix A
for (int i = 0; i < 4; i++)
I[i] = _mm_load_si128((__m128i *) (&l.values[i + c_row][k]));
// Read matrix B
for (int i = 0; i < 4; i++)
R[i] = _mm_set_epi32(
r.values[k][i + c_col], r.values[k + 1][i + c_col],
r.values[k + 2][i + c_col], r.values[k + 3][i + c_col]);
for (int i = 0; i < 4; i++) {
// Inner product of vector from matrix A and B
for (int j = 0; j < 4; j++)
T[j] = _mm_mullo_epi32(I[i], R[j]);
for (int j = 0; j < 2; j++) {
S[j] = _mm_unpacklo_epi32(T[j * 2], T[j * 2 + 1]);
S[j + 2] = _mm_unpackhi_epi32(T[j * 2], T[j * 2 + 1]);
}
for (int j = 0; j < 2; j++) {
D[j] = _mm_unpacklo_epi64(S[2 * j], S[2 * j + 1]);
D[j + 2] = _mm_unpackhi_epi64(S[2 * j], S[2 * j + 1]);
}
for (int j = 0; j < 4; j++)
Sum[i] = _mm_add_epi32(Sum[i], D[j]);
}
}
for (int i = 0; i < 4; i++)
_mm_store_si128((__m128i *) (&dst.values[c_row + i][c_col]), Sum[i]);
}
```
Reference :
- [Intel Instrinsics Guide](https://software.intel.com/sites/landingpage/IntrinsicsGuide/)
- [Wiki-SSE](https://zh.wikipedia.org/wiki/SSE)
### SIMD-AVX
- AVX(Advanced Vector Extensions) 指令集為 SSE 指令集的擴充。與 SSE 不同的是其有 16 個 256 bit 的暫存器。
- 既然暫存器的大小是 256 bit,我們可以一次計算 $8*8$ 大小的矩陣。
- 這裡用的方法與 cache-friendly 一樣,==與 sub-matrix 不同==。可以參考 [AVX matrix-multiplication, or something like it](http://www.mindfruit.co.uk/2012/02/avx-matrix-multiplication-or-something.html) 一文的做法。
- 所有的指令可以參考 [Intel Instrinsics Guide](https://software.intel.com/sites/landingpage/IntrinsicsGuide/),而我們會用到的指令有以下幾個。(記得先 `#include <immintrin.h>`)
- __m256i _mm256_setzero_si256 (void)
將 256 bit 全部設為 0。
- __m256i _mm256_loadu_si256 (__m256i const * mem_addr)
從 mem_addr 讀取 256 bit 的整數資料,不用考慮對齊。
- __m256i _mm256_set1_epi32 (int a)
將 256 bit 內存的 8 個 integer 都設成 `a` 。
- __m256i _mm256_mullo_epi32 (__m256i a, __m256i b)
向量 `a` 與 `b` 內積。
- __m256i _mm256_add_epi32 (__m256i a, __m256i b)
向量 `a` 與 `b` 相加。
- void _mm256_storeu_si256 (__m256i * mem_addr, __m256i a)
將 `a` 的內容存入 MEM[mem_addr] ,不用考慮對齊。
假設我們要計算 $[C]=[A]*[B]$ ,以下說明如何計算 $[C]$ 最上方 row 的結果,類推即可得完整的矩陣$[C]$。
1. 我們將 $[A]$ 、 $[B]$ 矩陣切成 $8*8$ 的 sub-matrix ,然候按照下圖同顏色做乘積。
![](https://i.imgur.com/4KNSROR.gif)
2. 假設 $[A]_{xy}$ 為 $[A]$ 矩陣中 row x, col y 的元素;$[B]_x$ 為 $[B]$ 矩陣中第 x 個 row 的向量。乘積後我們會有 8 個向量 $V_{1x}=[A]_{1x}*[B]_{x}$ , $x\in\{1,2,3,4,5,6,7,8\}$。
==$[B]_x=([B]_{x1},[B]_{x2},[B]_{x3},[B]_{x4},[B]_{x5},[B]_{x6},[B]_{x7},[B]_{x8})$==
![](https://i.imgur.com/55QabAD.gif)
3. 由上圖可看出,加總 8 個向量即可得 $[C]$ 矩陣第一個 row 的結果,剩下的 row 依序將 $A_{12}, A_{13}...,A_{18}$ 代入即可得。
完整程式碼如下:
```clike=1
void SIMD_AVX_matmul8(const Matrix l,
const Matrix r,
const Matrix dst,
int c_row,
int c_col)
{
__m256i I[8], R[8], S[8], Sum[8];
for (int i = 0; i < 8; i++)
Sum[i] = _mm256_setzero_si256();
for (int k = 0; k < l.col; k += 8) {
for (int i = 0; i < 8; i++)
R[i] = _mm256_loadu_si256((__m256i *) (&r.values[k + i][c_col]));
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 8; j++) {
I[j] = _mm256_set1_epi32(l.values[c_row + i][k + j]);
S[j] = _mm256_mullo_epi32(R[j], I[j]);
}
for (int j = 0; j < 8; j++)
Sum[i] = _mm256_add_epi32(Sum[i], S[j]);
}
}
for (int i = 0; i < 8; i++)
_mm256_storeu_si256((__m256i *) (&dst.values[c_row + i][c_col]),
Sum[i]);
}
void SIMD_AVX_matmul(const Matrix l,
const Matrix r,
const Matrix dst,
void *ctx)
{
CHECK(l, r, dst);
for (int i = 0; i < l.row; i += 8)
for (int j = 0; j < r.col; j += 8)
SIMD_AVX_matmul8(l, r, dst, i, j);
}
```
## SSE 指令集與 AVX 指令集的對齊問題
從 [Intel Instrinsics Guide](https://software.intel.com/sites/landingpage/IntrinsicsGuide/) 內對於這兩種指令集讀取連續記憶體的 function 說明中分別寫著
- __m128i _mm_load_si128 (__m128i const* mem_addr)
:::info
Load 128-bits of integer data from memory into dst. mem_addr must be aligned on a ==16-byte== boundary or a general-protection exception may be generated.
:::
- __m256i _mm256_load_si256 (__m256i const * mem_addr)
:::info
Load 256-bits of integer data from memory into dst. mem_addr must be aligned on a ==32-byte== boundary or a general-protection exception may be generated.
:::
兩者對於對齊的要求不同,在我的電腦(x86_64, 64 bit)下,每一次成功的 malloc 都會得到一個對齊 16-byte 的記憶體位置,但不見得會對齊 32-byte 。
從我的電腦連續 malloc 10000 次,每次都向系統要求 1~10 個 `sizeof(int)`,實驗的結果如下圖,每一次都可以得到對齊 16-byte 的記憶體位置,但只有大約一半次數會對齊 32-byte 。
![](https://i.imgur.com/iuT0crh.png)
實驗的程式碼如下
```clike=1
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#define VSIZE 10000
struct AlignTester {
int *ptr;
int iSize;
};
typedef struct AlignTester Tester;
int main()
{
int iAlign16 = 0, iAlign32 = 0;
Tester box[VSIZE];
for (int i = 0; i < VSIZE; i++) {
box[i].iSize = (rand() % 10)+1;
box[i].ptr = (int *) malloc(sizeof(int) * box[i].iSize);
if (((intptr_t) box[i].ptr) % 16 == 0)
iAlign16++;
if (((intptr_t) box[i].ptr) % 32 == 0)
iAlign32++;
}
printf("1\t%.2f\t2\t%.2f\t0.5\n", (double) iAlign16 / VSIZE * 100.0,
(double) iAlign32 / VSIZE * 100.0);
for (int i = 0; i < VSIZE; i++)
free(box[i].ptr);
return(0);
}
```
對於這種情況,SSE 指令集不會有問題,但 AVX 指令集會有一半的機率遇到 segmentation fault,解決這個問題可以有兩種做法。
- 改用不需要對齊的 function ,如用上一節使用的 `_mm256_loadu_mi256`。
- 把 `malloc` 換成 `posix_memalign` 函式,可以使取得的空間對齊 32-byte 。但是如果矩陣的大小並不是 8 的倍數,也就是會有 sub-matrix 不是完整的 $8*8$ ,還是會出現 segmentation fault 。
## 當使用 AVX 指令集時,矩陣大小並非 8 的倍數
有兩種情況
- 右方接近邊界,無法填滿 256 個 bit 。
- 下方接近邊界,無法產生 8 個向量加總。
後者較為單純,邏輯不變,只要修改迴圈上限即可;前者需將右方不足的部分填 0 ,將其視為 $8*8$ 的矩陣計算,為了避免讀取超出矩陣範圍造成 segmentation fault ,額外使用以下幾個指令做處理。
- __m256i _mm256_maskload_epi32 (int const* mem_addr, __m256i mask)
- __m256i _mm256_setr_epi32 (int e7, int e6, int e5, int e4, int e3, int e2, int e1, int e0)
- void _mm256_maskstore_epi32 (int* mem_addr, __m256i mask, __m256i a)
可接受矩陣大小非 8 的倍數版本如下:
```clike=1
void SIMD_AVX_matmul8(const Matrix l,
const Matrix r,
const Matrix dst,
int c_row,
int c_col)
{
__m256i I[8], R[8], S[8], Sum[8];
__m256i mask;
for (int i = 0; i < 8; i++)
Sum[i] = _mm256_setzero_si256();
for (int k = 0; k < l.col; k += 8) {
for (int i = 0; i < 8 && k + i < r.row; i++) {
if (c_col + 8 <= r.col)
R[i] =
_mm256_loadu_si256((__m256i *) (&r.values[k + i][c_col]));
else {
int iMask[8];
for (int j = 0; j < 8; j++)
iMask[j] = c_col + j < r.col ? 1 << 31 : 0;
mask =
_mm256_setr_epi32(iMask[0], iMask[1], iMask[2], iMask[3],
iMask[4], iMask[5], iMask[6], iMask[7]);
R[i] = _mm256_maskload_epi32((int *) (&r.values[k + i][c_col]),
mask);
}
}
for (int i = 0; i < 8 && c_row + i < l.row; i++) {
for (int j = 0; j < 8 && k + j < l.col; j++) {
I[j] = _mm256_set1_epi32(l.values[c_row + i][k + j]);
S[j] = _mm256_mullo_epi32(R[j], I[j]);
}
for (int j = 0; j < 8 && k + j < l.col; j++)
Sum[i] = _mm256_add_epi32(Sum[i], S[j]);
}
}
for (int i = 0; i < 8 && c_row + i < l.row; i++) {
if (c_col + 8 <= r.col)
_mm256_storeu_si256((__m256i *) (&dst.values[c_row + i][c_col]),
Sum[i]);
else
_mm256_maskstore_epi32(&dst.values[c_row + i][c_col], mask, Sum[i]);
}
}
void SIMD_AVX_matmul(const Matrix l,
const Matrix r,
const Matrix dst,
void *ctx)
{
CHECK(l, r, dst);
for (int i = 0; i < l.row; i += 8)
for (int j = 0; j < r.col; j += 8)
SIMD_AVX_matmul8(l, r, dst, i, j);
}
```
---
## 實作成果
實驗環境
```
$ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 8
On-line CPU(s) list: 0-7
Thread(s) per core: 2
Core(s) per socket: 4
Socket(s): 1
NUMA node(s): 1
Vendor ID: GenuineIntel
CPU family: 6
Model: 158
Model name: Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz
Stepping: 9
CPU MHz: 826.723
CPU max MHz: 3800.0000
CPU min MHz: 800.0000
BogoMIPS: 5616.00
Virtualization: VT-x
L1d cache: 32K
L1i cache: 32K
L2 cache: 256K
L3 cache: 6144K
NUMA node0 CPU(s): 0-7
```
以下下實驗矩陣大小均爲 1024
1. make
```bash=
$ make
gcc --std=gnu99 -O2 -DNDEBUG -msse4.1 -Wall -c -o main.o main.c
gcc --std=gnu99 -O2 -DNDEBUG -msse4.1 -Wall -mavx -march=native -c matmul.c
gcc --std=gnu99 -O2 -DNDEBUG -msse4.1 -Wall -c -o matrix.o matrix.c
gcc --std=gnu99 -O2 -DNDEBUG -msse4.1 -Wall -c -o strassen.o strassen.c
gcc --std=gnu99 -O2 -DNDEBUG -msse4.1 -Wall -o main main.o matmul.o matrix.o strassen.o
```
2. `./main`
- 由使用者自行選擇要使用的矩陣乘法
```bash=
$ ./main
------------------------------
Print matrix?
0: No
1: Yes
Please input value from 0 to 1: 0
------------------------------
Square matrix?
0: No
1: Yes
Please input value from 0 to 1: 1
------------------------------
Matrix size?
Please input value from 1 to 4096: 1024
Matrix multiplication: (1024 x 1024) x (1024 x 1024)
------------------------------
Choose a matrix multiplication method
0: quit
1: naive
2: cache friendly naive
3: submatrix
4: simd
5: simd_avx
6: strassen
Please input value from 0 to 6: 3
------------------------------
Stride?
Please input value from 2 to 4096: 4
Submatrix(stride=4)
correct!
CPU time: 3.106043 seconds
```
3. make test
- 提供使用者隨意輸入矩陣大小
- 由程式亂數創出該大小的矩陣
- 得知每個相乘方法的時間長度
```bash=
$ make test
./main --test < test_script/test.txt
Matrix multiplication: (1024 x 1024) x (1024 x 1024)
Naive
correct!
CPU time: 2.213579 seconds
-----------------------------------------------
Cache_friendly_naive
correct!
CPU time: 0.595068 seconds
-----------------------------------------------
Submatrix(stride=4)
correct!
CPU time: 1.246918 seconds
-----------------------------------------------
Submatrix(stride=6)
correct!
CPU time: 1.169062 seconds
-----------------------------------------------
SIMD
correct!
CPU time: 0.654296 seconds
-----------------------------------------------
SIMD_AVX
correct!
CPU time: 0.238938 seconds
-----------------------------------------------
Strassen(threshold=128)+Submatrix(stride=6)
correct!
CPU time: 0.543757 seconds
-----------------------------------------------
Strassen(threshold=128)+SIMD_AVX
correct!
CPU time: 0.195370 seconds
-----------------------------------------------
```
4. make perf
用 fork() 的方式,達到測試每個方法的 cache miss
- child 去做 perf 的指令
- 做完之後 parent 就 kill 掉他
**Code:**
- 利用 makefile 在編譯時加上 -DPERF
```clike=
#if defined(PERF)
int pid = getpid();
int cpid = fork();
if (cpid == 0) {
// child process . Run perf stat
char buf[128];
sprintf(buf, "perf stat -e cache-misses,cache-references,"
"instructions,cycles,branches,branch-misses -p %d", pid);
execl("/bin/sh", "sh", "-c", buf, NULL);
}
setpgid(cpid, 0);
#endif
clock_t tic = clock();
matmuls[mm](m, n, o, &matmul_ctx);
clock_t toc = clock();
#if defined(PERF)
kill(-cpid, SIGINT);
wait(&(int){0});
#endif
```
**Output:**
```bash=
Matrix multiplication: (1024 x 1024) x (1024 x 1024)
Performance counter stats for process id '4064':
61,190,796 cache-misses:u # 3.626 % of all cache refs
1,687,460,171 cache-references:u
8,428,134,785 instructions:u # 0.83 insn per cycle
10,161,492,593 cycles:u
1,055,317,279 branches:u
1,028,966 branch-misses:u # 0.10% of all branches
2.735073074 seconds time elapsed
Naive
correct!
CPU time: 2.797990 seconds
###############################################
Performance counter stats for process id '4064':
5,209,865 cache-misses:u # 4.085 % of all cache refs
127,541,989 cache-references:u
7,209,871,205 instructions:u # 3.39 insn per cycle
2,128,038,207 cycles:u
1,032,419,939 branches:u
1,005,070 branch-misses:u # 0.10% of all branches
0.576083717 seconds time elapsed
Cache-friendly-naive
correct!
CPU time: 0.604449 seconds
###############################################
Performance counter stats for process id '4064':
18,270,261 cache-misses:u # 4.643 % of all cache refs
393,530,544 cache-references:u
12,839,811,376 instructions:u # 2.56 insn per cycle
5,018,055,509 cycles:u
2,010,377,946 branches:u
77,591 branch-misses:u # 0.00% of all branches
1.354679373 seconds time elapsed
Submatrix(stride=4)
correct!
CPU time: 1.382032 seconds
###############################################
Performance counter stats for process id '4064':
25,508,482 cache-misses:u # 8.113 % of all cache refs
314,407,369 cache-references:u
11,108,525,213 instructions:u # 2.36 insn per cycle
4,704,450,795 cycles:u
1,647,827,769 branches:u
4,975,617 branch-misses:u # 0.30% of all branches
1.287659037 seconds time elapsed
Submatrix(stride=6)
correct!
CPU time: 1.314957 seconds
###############################################
Performance counter stats for process id '4064':
15,755,922 cache-misses:u # 5.458 % of all cache refs
288,694,157 cache-references:u
4,489,265,404 instructions:u # 1.55 insn per cycle
2,902,179,862 cycles:u
522,381,422 branches:u
81,643 branch-misses:u # 0.02% of all branches
0.795661427 seconds time elapsed
SIMD
correct!
CPU time: 0.828697 seconds
###############################################
Performance counter stats for process id '4064':
6,045,090 cache-misses:u # 9.784 % of all cache refs
61,783,562 cache-references:u
1,931,992,621 instructions:u # 1.90 insn per cycle
1,017,711,196 cycles:u
279,494,672 branches:u
1,976,689 branch-misses:u # 0.71% of all branches
0.281439317 seconds time elapsed
SIMD-AVX
correct!
CPU time: 0.312442 seconds
###############################################
```
## 實驗結果視覺化
### 不同矩陣乘法的運算時間
![](https://i.imgur.com/KVKjkUs.png)
### Strassen 演算法加速
Strassen 演算法在減少矩陣乘法計算量的同時會帶來一些 overhead,以下以結合 cache-friendly 方法爲例,實驗 Strassen 演算法中 threshold(不再進行切割的矩陣大小) 的選擇對運算時間的影響:
![](https://i.imgur.com/Y7PFQcJ.png)
Strassen 結合不同演算法帶來的加速效果:
![](https://i.imgur.com/TAg6wBl.png)
可以看到 naive 和 cache-friendly 的方法在結合 Strassen 之後時間相差不大,表示在 64*64 的矩陣乘法中,使用 naive 和 cache-friendly 的方法在運算時間上差異不大。
### Submatrix 切不同大小對運算時間的影響
![](https://i.imgur.com/P4W82jU.png)
![](https://i.imgur.com/i9UJG1O.png)
## Matrix Multiplication using SIMD 討論區
> 覺得可以直接先看上一組的影片,講得很清楚。
剛好之前有做過類似的([matrix-multiplication-parallel](https://github.com/siahuat0727/matrix-multiplication-parallel),有一些部分還寫不好),所以比較有概念。
另外,因爲 Strassen 只是 divide-and-conquer 後使得計算量減少,裡面還是會進行矩陣乘法,而這裡的乘法應該要可以自由選擇用 naive 或 simd 之類的實作。但上一組的寫法不能方便的做到這些,所以目前把乘法宣告成:
`typedef Matrix (*MatrixMulFunc)(const Matrix, const Matrix, void*)`
其中的 void* 保留給需要的 function (如 Strassen 中用來指定用哪種乘法進行運算以及分割的最小大小,或是將來決定是否平行運算之類的)。
對此大概寫了一點框架 [matrix-multiplication](https://github.com/siahuat0727/matrix-multiplication/tree/a866e5808e7b9a19fff01d62308150765825610f),不過不確定設計得好不好,有問題可以討論看看~
想說這部分要先達到共識之後才比較方便?
> [name=siahuat0727]
---
```c=1
MatrixMulFuncEle *matmul_listadd(MatrixMulFuncEle *list, MatrixMulFunc func,
char *name, void *ctx, size_t extra_size)
{
MatrixMulFuncEle *node = malloc(sizeof(MatrixMulFuncEle) + extra_size);
return_val_if_fail(node != NULL, 0);
node->matmul = func;
node->name = name;
memcpy(node->ctx, ctx, extra_size);
return node;
}
```
> 這裡 extra_size 打算用來做什麼?
> ctx 指的是?
> [name=pjchiou]
> ctx 指 context。
> 先說上一組的 sse 和 sse_prefetch,沒錯的話兩者差別不大,一大段的程式碼是複製貼上的,因爲他們是以一個 function 爲單位,不好修改。
> 所以想說給每種方法保留調整的空間,像是 sse 是否進行 prefetch,那這時需要傳一個 bool 給它。又或是 strassen 要用哪種矩陣乘法還有最小切割單位是多少之類的,這時就可以傳一個 struct 去控制,這些額外的空間就需要 extra_size 去存。
> 因爲最後要串起來,API 要一致。
[name=siahuat0727]
>> ok 明白了
>> [name=pjchiou]
---
```c=1
Matrix matrix_free(const Matrix m)
{
free(m.values[0]);
free(m.values);
return (Matrix){0};
}
```
> 這邊返回 `Matrix{0}` 的用意是?
> [name=pjchiou]
> 這裡是參考 [你所不知道的C語言:技巧篇](https://hackmd.io/s/HyIdoLnjl#%E5%BE%9E%E7%9F%A9%E9%99%A3%E6%93%8D%E4%BD%9C%E8%AB%87%E8%B5%B7) 提到的 [prefer-to-return-a-value-rather-than-modifying-pointers](https://github.com/mcinglis/c-style#prefer-to-return-a-value-rather-than-modifying-pointers)。
>:::info
>**Prefer to return a value rather than modifying pointers**
>This encourages immutability, cultivates pure functions, and makes things simpler and easier to understand. It also improves safety by eliminating the possibility of a NULL argument.
>```clike
>// Bad: unnecessary mutation (probably), and unsafe
>void drink_mix( Drink * const drink, Ingredient const ingr ) {
> assert( drink != NULL );
> color_blend( &( drink->color ), ingr.color );
> drink->alcohol += ingr.alcohol;
>}
>
>// Good: immutability rocks, pure and safe functions everywhere
>Drink drink_mix( Drink const drink, Ingredient const ingr ) {
> return ( Drink ){
> .color = color_blend( drink.color, ingr.color ),
> .alcohol = drink.alcohol + ingr.alcohol
> };
>}
>```
>This isn't always the best way to go, but it's something you should always consider.
>:::
>因爲應該要把 matrix 的 row 和 column 也設成 0,所以用的時候會是 `m = matrix_free(m)` 而不是 `matrix_free(&m)`
>其實我也是第一次這麼寫,想說試試看?
>不過我這裡還是少了 `assert(m.values!=NULL)` 或類似的處理。
[name=siahuat0727]
>> 感覺這個範例跟我們 `matrix_free` 的使用時機不大一樣,但我一時說不上哪不同。
>> [name=pjchiou]
>> 嗯嗯我也糾結了很久,都可以討論看看。
>> [name=siahuat0727]
>> 在想不一樣的地方是不是因爲我們的 struct 需要 allocate 空間?
>> 覺得比較麻煩的是用 `Matrix matmul(const Matrix, const Matrix, void*)` 這種寫法每次乘出來的 Matrix 都需要被 free,這有點討厭。
>> 如果用 `void matmul(const Matrix, const Matrix, Matrix *dst, void*)` 的話雖然要去檢查 dst 空間是不是已經配置過了之類的,但就可以一直重複使用同一個 dst,最後不用時再 free,不知道會不會比較好。
>> [name=siahuat0727]
>>> 我認為都可以,隨意選一種就好,我們把重點擺在運算的部份,反正分配記憶體的時間不算在我們要量測的範圍內。我們把其他方法也直接寫在 `naive.[hc]` 內好嗎?
>>> [name=pjchiou]
>>>> 也是,計時是夾整個 function,第一種方法會把分配的時間算進去,那我們就用第二種然後 function 裡假設 dst 已經被分配了而且大小是正確的吧。合在一起感覺大家一起寫的時候有點麻煩,我先去改成第二種的,如果覺得還是合在一起比較好的話我也都 ok。
>>>> [name=siahuat0727]
---