# 強化的 datalab
contributed by < `plusline` , `jason53415` , `butastur-rtos` >
github: [countbit](https://github.com/plusline/statistic) [cache_mountain](https://github.com/plusline/CpuCacheMountainViewer)
## 統整 Homework5,並提出更低 cycle count 的實作
* 用 perf 計算 cycles
* 提出更低 cycle count 的實作
### 工具及研究方法
#### 用 perf 計算 cycles
For example:
```C
int bitAnd(int x, int y)
{
return ~(~x | ~y);
}
void main(int argc, char **argv){
bitAnd(3,6);
}
```
> 加上 [force inline](https://jijithchandran.blogspot.com/2013/12/force-inline-functions-in-cgcc.html),降低函式呼叫的成本
> :notes: jserv
> > 能否用 macro ?
> > [name=butastur-rtos][time=Sat, Nov 3, 2018 2:11 PM]
```shell
$ gcc -o bitAnd bitAnd.c -m32 -O0
$ perf stat -e cycles -r 10000 ./bitAnd
Performance counter stats for './bitAnd' (10000 runs):
497,556 cycles ( +- 0.05% )
0.000329232 seconds time elapsed ( +- 0.11% )
```
#### 提出更低 cycle count 的實作
##### 原版:參數 -O0(不做最佳化)
```shell
Performance counter stats for './bitAnd' (10000 runs):
494,228 cycles ( +- 0.05% )
0.000631020 seconds time elapsed ( +- 0.13% )
```
##### 原版:參數 -O2(以速度做最佳化)
```shell
Performance counter stats for './bitAnd' (10000 runs):
492,721 cycles ( +- 0.06% )
0.000612500 seconds time elapsed ( +- 0.18% )
```
似乎有改善一點點
##### 嘗試加上 inline:無最佳化
```C
#include <stdio.h>
inline int bitAnd(int x, int y);
int bitAnd(int x, int y)
{
return ~(~x | ~y);
}
void main(int argc, char **argv){
bitAnd(3,6);
return;
}
```
```shell
root@plusline-System-Product-Name:~/plusline/group# perf stat -e cycles -r 10000 ./bitAnd
Performance counter stats for './bitAnd' (10000 runs):
484,403 cycles ( +- 0.11% )
0.000537299 seconds time elapsed ( +- 0.32% )
```
由 494,228 cycles 進步到 484,403,大約 2%,才加一個關鍵字就能有如此效益,感覺還不錯
##### 試試看 butastur-rtos 所說的 macro
```C
#include <stdio.h>
#define bitAnd(x,y) (~(~x|~y))
void main(int argc, char **argv){
bitAnd(3,6);
return;
}
```
```
root@plusline-System-Product-Name:~/plusline/group# perf stat -e cycles -r 10000 ./bitAnd
Performance counter stats for './bitAnd' (10000 runs):
490,227 cycles ( +- 0.07% )
0.000579284 seconds time elapsed ( +- 0.24% )
```
居然 macro 輸了,明明名字聽起來比較厲害。
> 不該只做單次比較,要考慮誤差範圍
> [name="jserv"]
##### macro 與 inline 差異
[Difference Between Inline and Macro in C++](https://techdifferences.com/difference-between-inline-and-macro.html) 提到10點 macro 與 inline 的差別。
* inline 是由編譯器解析; macro 是由預處理器解析。
* 一個關鍵字是 `inline` ;一個是 `#define`。
* inline 可以被宣告在 class 裡面或外面;macro 則必須宣告在程式開頭。
* The argument passed to the inline functions are evalutaed only once while compilation whereas, the macros argument are evaluated each time a macro is used in the code.
:::info
>這一點不太清楚 each time 是什麼意思? 是指 inline 是在組合語言的時候就一次把所有 inline function展開; macro 是在程式執行時才展開嗎?
>[name=plusline][color=orange]
:::
> 為什麼不去讀 C 語言規格書呢?inline 是一種編譯器最佳化的提示,可搭配 SSA optimization 運用
> [name=jserv]
* inline 不一定會幫你展開; macro 則使命必達。
* 在 class 內的短函數會自動幫你 inline。
* inline 可以取到 class 的成員;但 macro 不行。(這有點難理解,不都是原地字串的展開再執行?)
* inline 用大括號終止 function;macro 換行就終結。但是想寫多行的 macro 也是可以的。[C 語言之多行巨集](https://tonytonyjan.net/2014/10/07/multi-line-c-macros/)有提供作法
* macro 不好除錯
* Being a function an inline function bind its members within a start and closed curly braces. On the other hand, macro do not have any termination symbol so, binding becomes difficult when macro contains more that one statements.
:::info
>這裡的 binding becomes difficult when macro contains more that one statements 是什麼意思?是因為如同[C 語言之多行巨集](https://tonytonyjan.net/2014/10/07/multi-line-c-macros/)中提到的,macro在換行的操作很容易出錯,所以困難?
>[name=plusline][color=orange]
:::
> macro 無法做有效的參數型態檢查,更沒辦法讓編譯器推導同等效果的型態,後者是編譯器最佳化很關鍵的部分
> [name="jserv"]
至於效能,參考[ An Inline Function is As Fast As a Macro](http://web.mit.edu/rhel-doc/3/rhel-gcc-en-3/inline.html),似乎不分上下,不過許多關於 macro 和 inline 的效能比較都會強調 macro 的缺點,都會得出 macro 不好維護,建議用 inline 的結論。
> 避免用「歪樓」這樣不具體的描述,工程講究證據和精準用語
> [name="jserv"]
:::info
已修正不當用語,並加強詞彙的準確性。
:::
###### absVal
第一版---符合原本題目規定
```
int absVal(int x)
{
int y = x>>31;
return (y^x) + (~y+1);
}
```
` 483,536 cycles `
第二版---`(~y+1)==-y`
```c
int absVal(int x)
{
int y = x>>31;
return (y^x) - y;
}
```
`482,650 cycles`
第三版---branch
```c
int absVal(int x)
{
if(x<0)
return -x;
return x;
}
```
`484,148 cycles`
第四版---使用 Elvis operator
```c
int absVal(int x)
{
return (x>0)?x:-x;
}
```
`482,785 cycles`
:::info
>cycles 這幾種的差異比誤差還小,感覺不到是否有真的優化。
> [name=plusline][color=#2288aa]
:::
> 你檢驗幾組數值輸入呢?不是單次「變快」就叫改善,這裡我們期待的是 branch-less 和常數時間運算
> [name="jserv"]
>
:::info
> 在後面的研究方法已經透過增加取樣引入統計方法改善問題。
> [name=plusline][color=#2288aa]
:::
##### 其他降低 cycle 的方法
[Tips for Optimizing C/C++ Code](https://people.cs.clemson.edu/~dhouse/courses/405/papers/optimize.pdf) 提供幾個改善效能的方法
1. 由 Amdahl’s Law 可以知道,要改善最常發生的情形,並且使最糟情形的運作時間合理。
2. 改善演算法能夠大大的改善瓶頸
3. 改善效能是一件耗時的事
4. 使用分支的代價是昂貴的,盡量在短函式上使用 inline;遞迴的代價高於迴圈;把迴圈寫在函式裡(出乎意料的建議);switch 比多個 if...else if 好。
5. 由於陣列在記憶體中的存放方式,導致多維陣列在連續存取時,index 的順序會影響效能。(這點之前的課程有提到過)
# 個案討論 countbit
## 實際案例
### Hamming weight
### Hamming distance
### papper
[Faster Population Counts Using AVX2 Instructions](https://arxiv.org/pdf/1611.07612.pdf)
## 實做
### 我的方法(inline and non-inline)
```c
int bitCount(int x)
{
int A = x;
int B = x >> 1;
int S = A ^ B; // count even number
int C = A & B; // count even number and shift left one bit
//*****
A = S;
B = S >> 2;
int S1_1 = A ^ B;
int C1_1 = A & B; // shift one
A = C;
B = C >> 2;
int S1_2 = A ^ B; // shift one
int C1_2 = A & B; // shift two
//*****
int d=15+(15<<16);
int dd=d+(d<<8);
int k=1+(1<<16);
int kk=k+(k<<8);
int kkk=kk+(kk<<4);
int aa1 = S1_1 & kkk;
int aa2 = C1_1 & kkk;
int aa3 = S1_2 & kkk;
int aa4 = C1_2 & kkk;
int Sum = aa1 + (aa2 << 1) + (aa3 << 1) + (aa4 << 2);
Sum = Sum + (Sum >> 4);
Sum = Sum & dd;
Sum = Sum + (Sum >> 8);
Sum = Sum + (Sum >> 16);
Sum = Sum & ((31<<1)+1);
return Sum;
}
```
### 參考 Linjingchun97 的作法 (solution2)
```c
int bitCount(int x) {
int count;
int tmpMask1 = (0x55)|(0x55<<8);
int mask1 = (tmpMask1)|(tmpMask1<<16); //得到掩码: 01010101……01010101
int tmpMask2 = (0x33)|(0x33<<8);
int mask2 = (tmpMask2)|(tmpMask2<<16); //得到掩码: 00110011……00110011
int tmpMask3 = (0x0f)|(0x0f<<8);
int mask3 = (tmpMask3)|(tmpMask3<<16); //得到掩码: 00001111……00001111
int mask4 = (0xff)|(0xff<<16); //得到掩码: 0000 0000 1111 1111 0000 0000 1111 1111
int mask5 = (0xff)|(0xff<<8); //得到掩码: 0000 0000 0000 0000 1111 1111 1111 1111
count = (x&mask1)+((x>>1)&mask1); //分别计算每组2位中,低位的1的个数;再移位求每组2位中,高位的1的个数;求和
count = (count&mask2)+((count>>2)&mask2); //两两相加
count = (count + (count >> 4)) & mask3; //同理,两两相加
count = (count + (count >> 8)) & mask4; //同理,两两相加
count = (count + (count >> 16)) & mask5; //同理,两两相加
return count;
}
---------------------
作者:Linjingchun97
来源:CSDN
原文:https://blog.csdn.net/a794767404/article/details/79251430
版权声明:本文为博主原创文章,转载请附上博文链接!
```
其實我們兩個方法很接近,但是實做出來的結果就有不小差距。我們一樣都是先計算相鄰的兩位元的數量,再來計算相鄰四位元的數量,然後相鄰八位元,一直到三十二位元。差別在 Linjingchun97 先將不用的位元先去掉,而我為了不讓沒有用的位元影響到其他正確的位元,我採用加法器的作法,先用加法器的作法做到相鄰四位元後才轉入與 Linjingchun97 的作法,直到看到 Linjingchun97 的作法之後我才意識到加法器的部份有點多餘。而效能在下圖,可以看到雖然都是常數時間的演算法,僅僅就減少運算子,就能看到效能的提昇。
### macro + one by one [miguel-r-s/BitCounting](https://github.com/miguel-r-s/BitCounting)
```c
#define REPEAT4(x) x;x;x;x;
#define REPEAT2(x) x;x;
#define REPEAT32(x) REPEAT2(REPEAT4(REPEAT4(x)))
int bitCount(int x) {
unsigned n=(unsigned)x;
int counter = 0;
REPEAT32(counter += (n & 1);n>>=1;);
return counter;
}
```
這個作法是真的是一個一個數,重複的程式碼跑32次,因此加上 macro 提昇效能,但看結果也是不太好 。
![](https://i.imgur.com/MhvYy82.png)
### look up table [Bit Twiddling Hacks](http://graphics.stanford.edu/~seander/bithacks.html#IntegerLogDeBruijn)
```c
int bitCount(int x){
static const unsigned char BitsSetTable256[256] =
{
# define B2(n) n, n+1, n+1, n+2
# define B4(n) B2(n), B2(n+1), B2(n+1), B2(n+2)
# define B6(n) B4(n), B4(n+1), B4(n+1), B4(n+2)
B6(0), B6(1), B6(1), B6(2)
};
return BitsSetTable256[x & 0xff] +
BitsSetTable256[(x >> 8) & 0xff] +
BitsSetTable256[(x >> 16) & 0xff] +
BitsSetTable256[x >> 24];
}
```
這個作法是直接枚舉8個位元的所有可能,先在 itsSetTable256 算出每個8位元所能表示的值的1的個數, B2 是最低兩位,B4 是加上次低兩位, B6 再加上兩位,最後呼叫 define 4次,建構出8位元的表。
再把32位元切成4個8位元,分別計數後加總起來,就得到答案了。
![](https://i.imgur.com/EngYaSr.png)
```c
int bitCount4(int x){
static const unsigned char BitsSetTable[65536] =
{
# define B2(n) n, n+1, n+1, n+2
# define B4(n) B2(n), B2(n+1), B2(n+1), B2(n+2)
# define B6(n) B4(n), B4(n+1), B4(n+1), B4(n+2)
# define B8(n) B6(n), B6(n+1), B6(n+1), B6(n+2)
# define B10(n) B8(n), B8(n+1), B8(n+1), B8(n+2)
# define B12(n) B10(n), B10(n+1), B10(n+1), B10(n+2)
# define B14(n) B12(n), B12(n+1), B12(n+1), B12(n+2)
B14(0), B14(1), B14(1), B14(2)
};
return BitsSetTable[x&0xffff]+BitsSetTable[(x>>16)&0xffff];
}
```
![](https://i.imgur.com/OKnTB6M.png)
把table變大並沒有比較好,在最佳情形時的時間都一樣,但是最糟情形卻甚至會比使用分之還糟糕。
### 比較
方法: 依據中央極限定理,先求點估計,再查[t表](https://appwk.baidu.com/naapi/doc/view?ih=2306&o=png_6_0_0_0_0_909_1245_909_1245&iw=1685&ix=0&iy=0&aimw=1685&rn=1&doc_id=83bca9d1195f312b3169a5ad&pn=1&sign=45edb17c8d36258e6aa48cebb3b8bd35&type=1&app_ver=2.9.8.2&ua=bd_800_800_IncredibleS_2.9.8.2_2.3.7&bid=1&app_ua=IncredibleS&uid=&cuid=&fr=3&Bdi_bear=WIFI&from=3_10000&bduss=&pid=1&screen=800_800&sys_ver=2.3.7),算信賴區間,帶入有限母體信賴區間公式,$x±t\dfrac{s}{N}\sqrt{\dfrac{N-n}{N-1}}$ 。$N=2^{32},n=1000,t_{95\%}=1.96$
| Column 1 | 平均時間 x (ns) | 標準差 s | 95% 信賴區間
| -------- | -------- | -------- | -------- |
| non-pipeline | 120.325 | 10.7768 | 120.325±0.667953 |
| inline | 117.108 | 6.53489 |117.108±0.424398 |
| solution2 | 93.721 | 6.84727 |93.721±1.77908 |
| macro + one by one | 264.497 | 51.7969 | 264.497±3.21041 |
|:+1: look up table 8 | 73.104 | 6.06788 | 73.104±0.376091 |
| look up table 16 | 131.782 | 116.333 | 131.782±7.21042 |
# cache 對 lookup table 的影響
## 環境
```
root@node0:~/github_file/CpuCacheMountainViewer/data# lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 24
On-line CPU(s) list: 0-23
Thread(s) per core: 2
Core(s) per socket: 6
Socket(s): 2
NUMA node(s): 2
Vendor ID: GenuineIntel
CPU family: 6
Model: 45
Model name: Intel(R) Xeon(R) CPU E5-2620 0 @ 2.00GHz
Stepping: 7
CPU MHz: 2278.739
CPU max MHz: 2500.0000
CPU min MHz: 1200.0000
BogoMIPS: 4000.09
Virtualization: VT-x
L1d cache: 32K
L1i cache: 32K
L2 cache: 256K
L3 cache: 15360K
NUMA node0 CPU(s): 0-5,12-17
NUMA node1 CPU(s): 6-11,18-23
```
## 寄存器山實驗
![](https://i.imgur.com/fIs0wjN.png)
### 實驗方法
利用 [fabiensanglard](https://github.com/fabiensanglard/CpuCacheMountainViewer) 的專案進行讀取吞吐量的實驗,在用 gnuplot 畫出統計圖。
注意:這一個部份的取樣極少,部份極值的發生是抽樣的偏差造成的。
#### 實驗一
實驗目的:
觀察步長1~11加上資料大小32K~128M 吞吐量的的分布。
討論:隨著資料越大步長越長,吞吐量會越低,不過看起來跟課本的圖沒有很像,有可能是設備不一樣。課本使用
```
core i7 Haswell
2.1GHz
32KB L1 cache
256KB L2 cache
8MB L3 cache
```
![](https://i.imgur.com/T0oKEQl.png)
#### 實驗二 兩個核心
實驗目的:觀察只使用核心的數量會不會影響到吞吐量。
測量方式:
```
taskset -c 2 ./mountain
```
討論:跟課本的圖比較像一點,但還是看不出 L1快取 L2快取 L3快取 主記憶體,四個明顯的區段。在試試看增加 stride 和 size 看看。
![](https://i.imgur.com/mH4JnV1.png)
#### 實驗三 增加 stride
大功告成,加上等高線後,可以看到寄存器山的走勢。
![](https://i.imgur.com/A8rVaNx.png)
### cache-misses mountain
#### 實驗方法
修改[fabiensanglard](https://github.com/fabiensanglard/CpuCacheMountainViewer) 的專案,用 perf 抓取 L1 cache misses ,加上 sh 處理 perf 的輸出,再用 gnuplot 繪出。
#### 實驗一 total cache misses
討論:
沒想到居然不是步長越短 cache misses 越低,反倒是在步長10上下會是在低點。
![](https://i.imgur.com/GGh3jcd.png)
#### 實驗二 L1 cache misses
討論:
可以看出步長小資料小 L1 cache misses rate 會稍微小一點,在步長大資料量大時 misses rate 回大幅增加,不過在步長以及資料量中等時 misses rate 是差別不大的。
![](https://i.imgur.com/Oem2RPe.png)
#### 實驗三 LLC cache misses
討論:
last level cache (LLC) 在這裡指的是 L3 cache ,在這個測試環境中 L3 cache 是5360K ,可以對應到圖中的最低點。
![](https://i.imgur.com/XpLjIm9.png)
### 結合 countbit 的 table 分析
#### 實驗一
實驗目的:
讀取 countbit 的 look-up table 是循序的,觀察隨著步長 L1 cache misses rate 的變化
討論:
在步長小於 L1 cache 的時候,隨著步長增加,misses rate 也會隨著增加,至於超過 L1 cache 容量之後,大部分的情形會讓 cache misses rate 維持在接近50的位置,但讓我好奇的事,到底是什麼原因造成 misses rate 的抖動,而不是維持在一個定值?
![](https://i.imgur.com/x30FE6o.png)
#### 實驗二
實驗目的:
造成實驗一 cahe misses rate 抖動的原因是取樣不夠嗎?我抓取步長100到120的區間,增加取樣頻率,比較取樣頻率10(原本)和取樣頻率1000差異。
討論:
這段資料有明顯的抖動,而從這張圖看得出來,兩種不同的取樣頻率所繪出的曲線是高度重合的,因此推論這樣的抖動不是取樣頻率造成。
![](https://i.imgur.com/yJP6q47.png)
#### 實驗三
實驗目的:
比較 countbit 中兩種不同的大小 lootup table 在循序情況下的效能 。
討論:
在尋序讀 table 的情況下,8 bits 的 table cache misses 是非常低的。反觀 16 bits ,則是會隨著步長提昇 misses rate 。
![](https://i.imgur.com/wVvFXyR.png)
#### 實驗四
實驗目的:
比較 countbit 中兩種不同的大小 lootup table 在亂序情況下的效能 。
討論:
在 countbit的實驗中,對於時間平均計算時間的統計如下,8 bits 的 lookup table 執行時間是較短的。
| Column 1 | 平均時間 x (ns) | 標準差 s | 95% 信賴區間
| -------- | -------- | -------- | -------- |
|:+1: look up table 8(index 256) | 73.104 | 6.06788 | 73.104±0.376091 |
| look up table 16(index 65526) | 131.782 | 116.333 | 131.782±7.21042 |
![](https://i.imgur.com/7aFzlZB.png)
![](https://i.imgur.com/BEx9IEA.png)
:::warning
致命問題:
1. 分析效能時,僅有單次或極少次數的測試,並未涵蓋給定的 2^32^ 個有效整數輸入 (單一參數的案例),需要有完整涵蓋 (但不是盲目測試,因為排列組合後可能會讓你們測試不完,所以你們需要做數學分析,減少非必要的測試,再看數值分佈)
2. 統計模型需要拿出來用,而且需要能和程式行為作出對應解釋;
3. 題目要求探討上述程式碼在真實世界中的應用,就該在 GitHub 找出相關開放原始碼專案予以分析和探討;
:::
## Reference
* [gnuplot Histograms](http://psy.swansea.ac.uk/staff/carter/gnuplot/gnuplot_histograms.htm)
* [gnuplot 示範](https://hackmd.io/s/Skwp-alOg#)
###### tags: `sysprog2018`