# 在計算機裡頭實踐演算法
###### tags: `sysprog`
:::info
主講人: [jserv](http://wiki.csie.ncku.edu.tw/User/jserv) / 課程討論區: [2016 年系統軟體課程](https://www.facebook.com/groups/system.software2016/)
:mega: 返回「[進階電腦系統理論與實作](http://wiki.csie.ncku.edu.tw/sysprog/schedule)」課程進度表
:::
「真正的發現之旅不在於尋求新的風景,而是在於擁有新的眼光。」
> “The real voyage of discovery consists not in seeking new landscapes, but in having new eyes.”
― 法國作家 Marcel Proust
# 21 世紀的變革
[ [source](https://www.facebook.com/shihhaohung/posts/1301538069888678) ]
* 1990 年代中期,大型 NUMA 平行計算系統架構曾發展極盛,但軟體的成熟度與研發速度跟不上系統挾摩爾定律前進的速度,剛好 x86 架構 PC 的效能、Linux 作業系統的泛用性也逼近當時的專業工作站、伺服器,而且二者相乘之後以價廉物美、超多人在用的優勢,讓 HPC 和 datacenter 的趨勢由 NUMA 轉為用許多廉價 PC 等級的機器相串聯而成的 cluster-based 大型系統。
* 事隔 20 年,NUMA 是否會再度興起呢?我認為在那些需要對 big data(或是過程中會產生big data)快速即時做錯綜複雜(交叉)分析的高價值應用而言,NUMA 是達到最高成就的必然之路。只是說,NUMA 其中的 processor-memory interconnect 以及 memory access protocol,比 20 年前有更多樣化的選擇;所謂的 shared memory 系統的定義,也可放寬,例如用 RDMA;而且 GPU、Vector、FPGA 等加速器也應該被涵蓋進來,這和我們探討過的 HSA 相關。
# 利用歐幾里得法 (也就是「輾轉相除法」) 找 GCD
(最大公因數; _Greatest Common Divisor_)
演算法:
1. 大數 := 大數 - 小數
2. 若「最大」和「最小」兩數相同,那麼它就是最大公因數,否則,回到第 1 步
示範:
GCD(48, 40)
```
48 - 40, 40 -> 8, 40
8 != 40, 於是重複上述動作: 40 - 8, 8 -> 32, 8
8 != 32, 於是重複上述動作: 32 - 8, 8 -> 24, 8
8 != 24, 於是重複上述動作: 24 - 8, 8 -> 16, 8
8 != 16, 於是重複上述動作: 16 - 8, 8 -> 8, 8
8 == 8 成立,於是 8 為 GCD
```
用 C 語言實現歐幾里得法: (v1)
```clike=
int findGCD(int a, int b) {
while (1) {
if (a > b) a -= b;
else if (a < b) b -= a;
else return a;
}
}
```
以 Intel C compiler 產生的機械碼來說: (Pentium 4)
|指令|數量|延遲|cycle count|
|:---:|:---:|:---:|:----:|
|減法|5 |1 |5 |
|比較|5 |1 |5 |
|分支|14 |1 |14 |
|其他|0 |1 |0 |
| | | |24 |
改成 mod 操作的 C 語言程式如下: (v2)
```clike=
int findGCD(int a, int b) {
while (1) {
a %= b;
if (a == 0) return b;
if (a == 1) return 1;
b %= a;
if (b == 0) return a;
if (b == 1) return 1;
}
}
```
以 Intel C compiler 產生的機械碼來說:
|指令 |數量|延遲|cycle count|
|:-----------:|:--:|:--:|:--------:|
|mod (整數除法) |2 |68 |136 |
|比較 |3 |1 |3 |
|分支 |3 |1 |3 |
|其他 |6 |1 |6 |
| | | |148 |
如果可排除整數除法和減法的負擔,得到以下實做: (v3)
```clike=
int findGCD(int a, int b) {
while (1) {
if (a > (b * 4)) {
a %= b;
if (a == 0) return b;
if (a == 1) return 1;
} else if (a >= b) {
a -= b;
if (a == 0) return b;
if (a == 1) return 1;
}
if (b > (a * 4)) {
b %= a;
if (b == 0) return a;
if (b == 1) return 1;
} else if (b >= a) {
b -= a;
if (b == 0) return a;
if (b == 1) return 1;
}
}
}
```
可得到更快的實做:
```
v1: 14.56s
v2: 18.55s
v3: 12.14s
```
* 以上數據出自: [The Software Optimization Cookbook: High Performance Recipes for IA-32 Platforms, 2nd Edition](http://www.amazon.com/The-Software-Optimization-Cookbook-Performance/dp/0976483211)
* 但這是 2004 年的表現,現在的電腦架構呢?
## 在 ARM Cortex-A8 上的實驗
* 實驗人: 張家榮
* 平台:BeagleBone Black (ARMv7-A, 1 GHz)
* 實驗程式碼:[github](https://github.com/JaredCJR/GCD_Efficiency_Compare)
* 採用組合語言撰寫 (GCD 的部份),由於 Cortex-A8 不支援 idiv (要在 ARM Cortex-A15 以上才有),所以 以 ARM 組合語言實做[計算機組織課本上的除法器的空間優化版本](https://coursejared.hackpad.com/ch4_Multiplier-and-Divider-wFnZyFI0E6I)。
* 發現gcc的除法似乎有針對2的倍數做判斷,移位(除法)
* 實做的版本尚未考慮exception
* 這裡有個[參考網站](http://me.henri.net/fp-div.html)列出了exception重點
### 實驗方法
* 圖上一個點,代表GCD (big, small) 中的 big,會尋找比它小的所有正整數(>1)的GCD所需的時間「總和」
* 如果有個 big=9995,那麼會找 9993 組 GCD,small 的範圍從 2 到 9994
* 如: (9995,2), (9995,3), (9995,4),......, (9995,9994)
* **所以big越大,找尋的組數越多,整體趨勢微微上升(從左到右)是正常的**
* 重點是比較各演算法的趨勢
### ARM vs. Thumb instruction
* ARM instruction速度較快
* 直線為curve fitting的結果(時間越低,效能越高)
![](https://hackpad-attachments.s3.amazonaws.com/embedded2015.hackpad.com_PxVP6UWvGxh_p.339782_1439772172448_undefined)
**Assembly**
```
MOD:
@ Entry r0: low (remainder low) must be postive<input:Dividend>
@ r1: high (remainder high) any number
@ r2: Divisor (divisor) must be non-zero and positive<input:Divisor>
low .req r0;
high .req r1;
divisor .req r2;
mov high,#0
.rept 33 @ repeat 33 times
subs high,high,divisor @remainder = remainder - divisor
@it lo
addlo high, high, divisor
adcs low, low, low @ low<<1; ((high-divisor)>0) ? (new bit=1) : (new bit=0)
adc high,high,high @ divisor>>1 == high(64bit for high&low)<<1
.endr
lsr high, #1 @shift remainder to correct position(because last divide don’t know this is the last divide,it still prepare for the next divide<shift>)
@ Exit r0: Quotient (remainder low)
@ r1: remainder (remainder high)
mov pc,lr
```
**Euclidean(thumb) V.S. mod(thumb) V.S. mod_minus(thumb)**
* 不符合預期的原因推估
* v3版本的branch過多(if,else),thumb的branch效能不佳,導致拉低平均
* if數量比較(一個loop)
* v1:3個
* v3:7個
![](https://hackpad-attachments.s3.amazonaws.com/embedded2015.hackpad.com_PxVP6UWvGxh_p.339782_1439770375774_undefined)
**Euclidean(arm) V.S. mod(arm) V.S. mod_minus(arm)**
* 比較
* 與thumb版本
* arm的branch效能較佳,所以v3版本速度改善最多,這裡符合預期
* v1輸給了v2
* arm的v1與thumb的v1相比,只有微微速度提昇(看第一張圖),但是mod卻提昇較多(幾乎是半張圖片的range),導致了這個結果
* curve fitting
![](https://hackpad-attachments.s3.amazonaws.com/embedded2015.hackpad.com_PxVP6UWvGxh_p.339782_1439741803792_undefined)
* qemu
* 符合預期
![](https://hackpad-attachments.s3.amazonaws.com/embedded2015.hackpad.com_PxVP6UWvGxh_p.339782_1439777549508_embedded2015.hackpad.com_PxVP6UWvGxh_p.png)
## C語言
* arm-angstrom-linux-gnueabi-gcc內的除法經過最佳化後,效能比減法更佳
![](https://hackpad-attachments.s3.amazonaws.com/embedded2015.hackpad.com_PxVP6UWvGxh_p.339782_1439777592642_embedded2015.hackpad.com_PxVP6UWvGxh_p%20(1).png)
* arm-linux-gnueabihf-gcc(linaro) (without vfpv3)
* 上下圖比較,可以看到整體在arm-angstrom-linux-gnueabi-gcc效能比較好
* 即使在這裡,單純除法比單純減法(的演算法)還快...
* 可是綜合起來看 v3更快(符合預測)
![](https://hackpad-attachments.s3.amazonaws.com/embedded2015.hackpad.com_PxVP6UWvGxh_p.339782_1440082584221_undefined)
* arm-linux-gnueabihf-gcc(linaro) (with vfpv3)
* mod加快,減法變慢
* 開了比沒開還慢...
![](https://hackpad-attachments.s3.amazonaws.com/embedded2015.hackpad.com_PxVP6UWvGxh_p.339782_1440118082155_undefined)
## 效能分析: Prefetching
[ [source](https://www.facebook.com/notes/champ-yen/the-power-of-prefetching/1214833498543546) ]
* 論文: "[When Prefetching Works, When It Doesn’t, and Why](http://www.cc.gatech.edu/~hyesoon/lee_taco12.pdf)"
* 用實例顯示 prefetching 帶來的效益,範例是 Matrix Transpose,一個簡單的 C 版本實作如下
```clike=
void naive_transpose(int *src, int *dst, int w, int h)
{
for (int x = 0; x < w; x++){
for (int y = 0; y < h; y++) {
*(dst + x*h + y) = *(src + y * w + x);
}
}
}
```
接著參考 [Programming trivia: 4x4 integer matrix transpose in SSE2](http://www.randombit.net/bitbashing/2009/10/08/integer_matrix_transpose_in_sse2.html) 實作 Intel SSE2 加速的版本,由於先進處理器架構有著 automatic/speculative prefetcher 所以這裡我們故意寫得比較 CPU-unfriendly 一些:
```clike=
void sse_transpose(int *src, int *dst, int w, int h)
{
for (int x = 0; x < w; x += 4) {
for (int y = 0; y < h; y += 4) {
__m128i I0 = _mm_loadu_si128 ((__m128i *)(src + (y + 0) * w + x));
__m128i I1 = _mm_loadu_si128 ((__m128i *)(src + (y + 1) * w + x));
__m128i I2 = _mm_loadu_si128 ((__m128i *)(src + (y + 2) * w + x));
__m128i I3 = _mm_loadu_si128 ((__m128i *)(src + (y + 3) * w + x));
__m128i T0 = _mm_unpacklo_epi32(I0, I1);
__m128i T1 = _mm_unpacklo_epi32(I2, I3);
__m128i T2 = _mm_unpackhi_epi32(I0, I1);
__m128i T3 = _mm_unpackhi_epi32(I2, I3);
I0 = _mm_unpacklo_epi64(T0, T1);
I1 = _mm_unpackhi_epi64(T0, T1);
I2 = _mm_unpacklo_epi64(T2, T3);
I3 = _mm_unpackhi_epi64(T2, T3);
_mm_storeu_si128((__m128i*)(dst+(x*h)+y), I0);
_mm_storeu_si128((__m128i*)(dst+((x+1)*h)+y), I1);
_mm_storeu_si128((__m128i*)(dst+((x+2)*h)+y), I2);
_mm_storeu_si128((__m128i*)(dst+((x+3)*h)+y), I3);
}
}
}
```
我們來撰寫一個使用 SSE prefetch 指令的加速版本:
```clike=
void sse_prefetch_transpose(int *src, int *dst, int w, int h)
{
for (int x = 0; x < w; x += 4) {
for (int y = 0; y < h; y += 4) {
#define** PFDIST 8**
_mm_prefetch(src + (y + PFDIST + 0) * w + x, _MM_HINT_T1);
_mm_prefetch(src + (y + PFDIST + 1) * w + x, _MM_HINT_T1);
_mm_prefetch(src + (y + PFDIST + 2) * w + x, _MM_HINT_T1);
_mm_prefetch(src + (y + PFDIST + 3) * w + x, _MM_HINT_T1);
__m128i I0 = _mm_loadu_si128 ((__m128i *)(src + (y + 0) * w + x));
__m128i I1 = _mm_loadu_si128 ((__m128i *)(src + (y + 1) * w + x));
__m128i I2 = _mm_loadu_si128 ((__m128i *)(src + (y + 2) * w + x));
__m128i I3 = _mm_loadu_si128 ((__m128i *)(src + (y + 3) * w + x));
__m128i T0 = _mm_unpacklo_epi32(I0, I1);
__m128i T1 = _mm_unpacklo_epi32(I2, I3);
__m128i T2 = _mm_unpackhi_epi32(I0, I1);
__m128i T3 = _mm_unpackhi_epi32(I2, I3);
I0 = _mm_unpacklo_epi64(T0, T1);
I1 = _mm_unpackhi_epi64(T0, T1);
I2 = _mm_unpacklo_epi64(T2, T3);
I3 = _mm_unpackhi_epi64(T2, T3);
_mm_storeu_si128((__m128i*)(dst + ((x + 0) * h) + y), I0);
_mm_storeu_si128((__m128i*)(dst + ((x + 1) * h) + y), I1);
_mm_storeu_si128((__m128i*)(dst + ((x + 2) * h) + y), I2);
_mm_storeu_si128((__m128i*)(dst + ((x + 3) * h) + y), I3);
}
}
}
```
接著是測試程式:
```clike=
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <sys/time.h>
//for using x86-SSE2 intrinsics you have to include this
#include <xmmintrin.h>
// PUT naive_transpose, sse_transpose, sse_prefetch_transpose HERE
// ...
#define TEST_W 4096
#define TEST_H 4096
int main(void)
{
//verify the result of 4x4 matrix
{
int testin[16] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 };
int testout[16];
for(int y = 0; y < 4; y++){
for(int x = 0; x < 4; x++)
printf(" %2d", testin[y*4+x]);
printf("\n");
}
printf("\n");
sse_transpose(testin, testout, 4, 4);
for(int y = 0; y < 4; y++){
for(int x = 0; x < 4; x++)
printf(" %2d", testout[y*4+x]);
printf("\n");
}
}
{
struct timeval stime, etime;
int *src =(int*)malloc(sizeof(int)*TEST_W*TEST_H);
int *out0 = (int*)malloc(sizeof(int)*TEST_W*TEST_H);
int *out1 = (int*)malloc(sizeof(int)*TEST_W*TEST_H);
int *out2 = (int*)malloc(sizeof(int)*TEST_W*TEST_H);
srand(time(NULL));
for(int y = 0; y < TEST_H; y++){
for(int x = 0; x < TEST_W; x++)
*(src + y*TEST_W + x) = rand();
}
gettimeofday(&stime, NULL);
sse_prefetch_transpose(src, out0, TEST_W, TEST_H);
printf("sse prefetch: %ld us\n", (etime.tv_sec - stime.tv_sec)*1000000 + (etime.tv_usec - stime.tv_usec));
gettimeofday(&stime, NULL);
sse_transpose(src, out1, TEST_W, TEST_H);
gettimeofday(&etime, NULL);
printf("sse: %ld us\n", (etime.tv_sec - stime.tv_sec)*1000000 + (etime.tv_usec - stime.tv_usec));
gettimeofday(&stime, NULL);
naive_transpose(src, out2, TEST_W, TEST_H);
gettimeofday(&etime, NULL);
printf("naive: %ld us\n", (etime.tv_sec - stime.tv_sec)*1000000 + (etime.tv_usec - stime.tv_usec));
free(src);
free(out0);
free(out1);
free(out2);
}
return 0;
}
```
存成 test.c, 並且按照下列方式編譯
$ gcc -msse2 -o test test.c
在 Intel Core i7 3612QM 執行得到下列結果:
```
0 1 2 3
4 5 6 7
8 9 10 11
12 13 14 15
0 4 8 12
1 5 9 13
2 6 10 14
3 7 11 15
sse prefetch: 57782 us
sse: 117184 us
naive: 228870 us
```
儘管現今 CPU 有著強大的 auto-prefetcher,但是並不是所有的演算都有著規律或是線型的記憶體存取模式, 如此透過優化 prefetching 依然是能夠有相當的效能增進.
在 EeePC 900 (Intel Celeron M 900 MHz) 仍有 22% 的效能差異:
```
sse prefetch: 676158 us
sse: 860861 us
naive: 1644733 us
```