# 2017q3 Homework1 (clz)
###### tags: `sysprogram`
contributed by <`st9007a`>
## [Github](https://github.com/st9007a/clz-tests)
### Reviewed by `amikai`
- [726ac7e](https://github.com/st9007a/clz-tests/commit/726ac7e50c35684aa63144192b5ebff22b775de8) 此 commit 做的是對整個架構的大修改, 建議加上修改的動機, 讓其他協作者 (如果很多人開發) 了解為什麼要有這些修改
- 8 bit, 16bit, 32bit 三張圖, 沒有對三張圖的比較做詳細的解釋, 講述了數據不穩定, 但沒有說緣由
以上為小弟簡短的建議, 如有冒犯請見諒
## 開發環境
```
Linux 4.4.0-92-generic
Ubuntu 16.04.1 LTS
L1d cache: 32K
L1i cache: 32K
L2 cache: 256K
L3 cache: 6144K
```
## 針對 32-bits 數值對各版本之 clz 比較效能
利用 fork 過來的程式,可以快速的實測各版本的 clz 對 32-bits 整數的效能比較
![](https://i.imgur.com/gGTUDwm.png)
從上圖可以知道 byte-shift 跟 binary search 所使用的 cycle 數平均是比較少的,然後 cycle 數 400 到 500 之間也有點分布,試著隨便測其他區間:
![](https://i.imgur.com/isOLevl.png)
![](https://i.imgur.com/Zlpgy4m.png)
分布狀況看起來都差不多,不過一部分的數據會跳到 400 至 500 cycles 之間,這是由於作業系統multi task,如果有其他 task 要優先值行的話,作業系統會先中斷現在執行的 task,等完成其他 task 在跳回來繼續執行,所以花費的 cycle 數會比較高
## 解析原始碼
執行完效能比較後,來看看 `clz.h` 裡面是怎麼實作個版本的 clz 演算法
### recursive
```c
static const int mask[] = {0, 8, 12, 14};
static const int magic[] = {2, 1, 0, 0};
//static inline __attribute((always_inline))
unsigned clz2(uint32_t x,int c)
{
// *INDENT-OFF*
if (!x && !c) return 32;
uint32_t upper = (x >> (16 >> c));
uint32_t lower = (x & (0xFFFF>>mask[c]));
if (c == 3) return upper ? magic[upper] : 2 + magic[lower];
return upper ? clz2(upper, c + 1) : (16 >> (c)) + clz2(lower, c + 1);
// *INDENT-ON*
}
```
`c` 這個變數紀錄每次遞迴的層數,每次遞迴會將 `x` 分成前半部( upper )跟後半部( lower ),如果前半部不為零,則回傳前半部的 clz,若前半部為零,則回傳前半部的 bit 數量加上後半部的 clz,而倒數第二行的停止條件則是第四次遞迴時,前半部跟後半部只剩 2 個 bits,所以直接回傳查表對應的 clz
### iteration
```c
static inline __attribute((always_inline))
unsigned clz(uint32_t x)
{
// *INDENT-OFF*
int n = 32, c = 16;
do {
uint32_t y = x >> c;
if (y) { n -= c; x = y; }
c >>= 1;
} while (c);
return (n - x);
// *INDENT-ON*
}
```
迴圈裡面的 `y` 用來記錄 `x` 的前半部,`n` 用來記錄 clz,如果前半部不為零就用前半部去做下一個迴圈,同時 `n` 減去後半部的 bit 的數量( 因為至少後半部的 bit 不會算進 clz ),迴圈做到最後當 `c` 變成 0 跳出迴圈時 `x` 還剩下一個 bit, `n` 直接減去最後一個 bit 的值就能得到 clz
### byte shift
```c
static inline __attribute((always_inline))
unsigned clz(uint32_t x)
{
// *INDENT-OFF*
if (x == 0) return 32;
int n = 1;
if ((x >> 16) == 0) { n += 16; x <<= 16; }
if ((x >> 24) == 0) { n += 8; x <<= 8; }
if ((x >> 28) == 0) { n += 4; x <<= 4; }
if ((x >> 30) == 0) { n += 2; x <<= 2; }
n = n - (x >> 31);
return n;
// *INDENT-ON*
}
```
`n` 用來記錄 clz,利用 if 來分別確認 `x` 的前 16 , 8 , 4 , 2 個 bits 是否為零,如果為零則將他們加入 `n`,並且左移 `x` 確保下個 if 能檢查到正確的位置,如果不為零則直接做下一個 if 檢查,最後 `x` 剩下 最前面的 1 bit 沒有被檢查到,做法跟 iteration 一樣,直接減去 bit 的值就是 clz
### binary search
```c
static inline __attribute((always_inline))
unsigned clz(uint32_t x)
{
// *INDENT-OFF*
if (x == 0) return 32;
int n = 0;
if (x <= 0x0000FFFF) { n += 16; x <<= 16; }
if (x <= 0x00FFFFFF) { n += 8; x <<= 8; }
if (x <= 0x0FFFFFFF) { n += 4; x <<= 4; }
if (x <= 0x3FFFFFFF) { n += 2; x <<= 2; }
if (x <= 0x7FFFFFFF) { n += 1; x <<= 1; }
return n;
// *INDENT-ON*
}
```
原理跟 byte shift 一樣,只是改成用比大小的方式來確認前 16 , 8 , 4 , 2 個 bits 是否為零,最後一個 if 取代 byte shift 版本的 `n = n - (x >> 31);` 檢查最前面的 bit
### harely
```c
static inline __attribute((always_inline))
unsigned clz(uint32_t x)
{
// *INDENT-OFF*
static uint8_t const Table[] = {
32, 31, 0, 16, 0, 30, 3, 0, 15, 0, 0, 0, 29, 10, 2, 0,
0, 0, 12, 14, 21, 0, 19, 0, 0, 28, 0, 25, 0, 9, 1, 0,
17, 0, 4, 0, 0, 0, 11, 0, 13, 22, 20, 0, 26, 0, 0, 18,
5, 0, 0, 23, 0, 27, 0, 6, 0, 24, 7, 0, 8, 0, 0, 0
};
// *INDENT-ON*
/* Propagate leftmost 1-bit to the right */
x = x | (x >> 1);
x = x | (x >> 2);
x = x | (x >> 4);
x = x | (x >> 8);
x = x | (x >> 16);
/* x = x * 0x6EB14F9 */
x = (x << 3) - x; /* Multiply by 7. */
x = (x << 8) - x; /* Multiply by 255. */
x = (x << 8) - x; /* Again. */
x = (x << 8) - x; /* Again. */
return Table[(x >> 26)];
}
```
從演算法名稱可以預期到` 0x6EB14F9` 是 De Bruijn 數列,一開始前五個 or 運算將 `x` 的最高有效位往右的 bit 都變成 1,接著可以從作業共筆知道存在著 De Bruijn 數列 M 使得 $(a \times M) >> n$ 出來的結果彼此不衝突,其中 $a = 0...000111...1_2$,在這裡 $M = 6EB14F9_{16} , n = 26$,接著 hash 出來的結果直接去查表就能得到 clz
## 修改程式碼
由於作業要求有提到 C11 的 `_Generic`,研究了一下把程式改成支援 8-bit, 16-bit, 32-bit, 64-bit unsigned 整數,首先將標頭檔套用 `_Generic`
```c
#ifndef CLZ_H
#define CLZ_H
#include <stdint.h>
#if defined(recursive)
#define clz(x) \
_Generic((x), \
uint8_t: clz_u8, \
uint16_t: clz_u16, \
uint32_t: clz_u32, \
uint64_t: clz_u64 \
)(x, 0)
unsigned clz_u8(uint8_t x, int c);
unsigned clz_u16(uint16_t x, int c);
unsigned clz_u32(uint32_t x, int c);
unsigned clz_u64(uint64_t x, int c);
#else
#define clz(x) \
_Generic((x), \
uint8_t: clz_u8, \
uint16_t: clz_u16, \
uint32_t: clz_u32, \
uint64_t: clz_u64 \
)(x)
unsigned clz_u8(uint8_t x);
unsigned clz_u16(uint16_t x);
unsigned clz_u32(uint32_t x);
unsigned clz_u64(uint64_t x);
#endif
#endif
```
接著分別在 `iteration.c`, `recursive.c`, `byte.c` , `binary.c` 根據原本的函式修改出 8-bit, 16-bit, 64-bit的版本,由於全部程式碼太多行,且只是按 32-bit 改出其它版本,所以就不附上程式碼,harely 還沒實作,需先找到對應的 De Bruijn 數列
改好程式碼後,把 Makefile 中的 `-std=gnu99` 改成 `-std=c11`,然後來編譯看看
```
cc -Wall -std=c11 -g -DDEBUG -O0 -o iteration \
-MMD -MF .iteration.d \
-Diteration main.c
main.c: In function ‘get_cycles’:
main.c:14:5: error: ‘asm’ undeclared (first use in this function)
asm volatile ("CPUID\n\t"
^
main.c:14:5: note: each undeclared identifier is reported only once for each function it appears in
main.c:14:9: error: expected ‘;’ before ‘volatile’
asm volatile ("CPUID\n\t"
^
main.c: In function ‘get_cycles_end’:
main.c:24:5: error: ‘asm’ undeclared (first use in this function)
asm volatile("RDTSCP\n\t"
^
main.c:24:9: error: expected ‘;’ before ‘volatile’
asm volatile("RDTSCP\n\t"
^
Makefile:30: recipe for target 'iteration' failed
make: *** [iteration] Error 1
```
會發現他不認得 `asm` 這個保留字,查了一下原因,知道了 `asm` 是 C 語言的 extension,要使用 extension 必須把 gcc 的 `-std=c11` 改成 `-std=gnu11`,接著再試著編譯看看
```
make: Warning: File 'Makefile' has modification time 0.76 s in the future
cc -Wall -std=gnu11 -g -DDEBUG -O0 -o iteration \
-MMD -MF .iteration.d \
-Diteration main.c
/tmp/ccU6IkJY.o: In function `main':
/home/st9007a/project/sysprog/clz-tests/main.c:107: undefined reference to `clz_u32'
collect2: error: ld returned 1 exit status
Makefile:30: recipe for target 'iteration' failed
make: *** [iteration] Error 1
```
發現它竟然認不得 `clz_u32`,所以打開 `Makefile` 看看是什麼狀況
```
%: $(SRCS_common) %.c clz.h
$(CC) $(CFLAGS_common) -o $@ \
-MMD -MF .$@.d \
-D$(shell echo $(subst .o,,$@)) $(SRCS_common)
```
原來實作 clz function 的檔案沒有被一起編譯,所以改成如下
```
%: $(SRCS_common) %.c clz.h
$(CC) $(CFLAGS_common) -o $@ \
-MMD -MF .$@.d \
-D$(shell echo $(subst .o,,$@)) $(SRCS_common) $@.c
```
在最後面加入 `$@.c`,把實作 clz function 的檔案一起編譯
```
$ make
cc -Wall -std=gnu11 -g -DDEBUG -O0 -o iteration \
-MMD -MF .iteration.d \
-Diteration main.c iteration.c
cc -Wall -std=gnu11 -g -DDEBUG -O0 -o binary \
-MMD -MF .binary.d \
-Dbinary main.c binary.c
cc -Wall -std=gnu11 -g -DDEBUG -O0 -o byte \
-MMD -MF .byte.d \
-Dbyte main.c byte.c
cc -Wall -std=gnu11 -g -DDEBUG -O0 -o recursive \
-MMD -MF .recursive.d \
-Drecursive main.c recursive.c
```
大功告成!來用原本的測試測試看看如何
![](https://i.imgur.com/f9uEymk.png)
跟先前的版本差不多,看來效能沒有因為我的修改被改爛,接著來測看看不同的bit width,首先把 `main.c` 做一點小修改
```c
#if defined(u8)
uint8_t i;
#elif defined(u16)
uint16_t i;
#elif defined(u32)
uint32_t i;
#else
uint64_t i;
#endif
```
把要測試的數字根據不同 bit width 宣告成不同型態,`Makefile` 也加入對應 flag 來自動化
```
bit_width ?= 32
from ?= 67100000
to ?= 67110000
CFLAGS_common += -Du$(bit_width)
run: $(EXEC)
for method in $(EXEC); do\
taskset -c 1 ./$$method $(from) $(to); \
done
```
### 8-bit 結果
![](https://i.imgur.com/OBftzix.png)
### 16-bit 結果
![](https://i.imgur.com/xxWFqIN.png)
### 64-bit 結果
![](https://i.imgur.com/1PosoUA.png)
可以看到效能是差不多的,8-bit 由於數字只有 256 個,所以只看 100 到 200 的範圍,其數據比較不穩定
## 應用場合
### Division Algorithm
參考 [A Fast Hi Precision Fixed Point Divide](http://me.henri.net/fp-div.html) 中,對於硬體實作的除法演算法要做 32 個 iterations 花費 97 個 cycles,程式如下:
```asm
@ Entry r0: numerator (lo) must be signed positive
@ r2: deniminator (den) must be non-zero and signed negative
idiv:
lo .req r0; hi .req r1; den .req r2
mov hi, #0 @ hi = 0
adds lo, lo, lo
.rept 32 @ repeat 32 times
adcs hi, den, hi, lsl #1
subcc hi, hi, den
adcs lo, lo, lo
.endr
mov pc, lr @ return
@ Exit r0: quotient (lo)
@ r1: remainder (hi)
```
在被除數與除數有 leading zero 的狀況下,這個演算法前幾個( 被除數與除數共有的leading zero 數量 ) iterations 就等同於只單純的左移 `hi` 跟 `lo` 這兩個暫存器,因此可以預先計算好 clz 然後先移動 `hi` , `lo` 來減少 iterations 的數量,參考資料中提到,在 ARM v5 架構下使用 clz 指令可以減少 9 到 18 個 cycles
### Integer Square Root
整數平方根的算法有很多種,這裡指的是牛頓法( Newton's Method ),牛頓法的概念是數字 $a$ 的平方根可以透過給定一個初始值 $g_0$ 並反覆計算以下公式來找到:
$g_{n+1} = (g_n + a / g_n)\times 0.5$
在計算過程中 $g$ 的值會趨近於 $a$ 的平方根,因此可以知道 $g_0$ 的值會影響運算時間,在 [Hacker's Delight](https://books.google.com.tw/books?id=VicPJYM0I5QC&printsec=frontcover&hl=zh-TW#v=onepage&q&f=false) 281頁中提到,應找大於等於 $a$ 之平方根的 $2 ^ n$ 整數作為 $g_0$,尋找的方法就可以透過 clz 去算出來
附上程式碼:
```clike
int isqrt(unsigned x)
{
int s, g0, g1;
if (x <= 1) return x;
s = 16 - clz(x - 1) / 2;
g0 = 1 << s;
g1 = (g0 + x >> s) >> 1;
while (g1 < g0) {
g0 = g1;
g1 = (g0 + x / g0) >> 1;
}
return g0;
}
```
關鍵在於 `s = 16 - clz(x - 1) / 2` 這個部分, `s` 的目的是算出最接近的數字符合 $2^s >= \sqrt{x}$,可以將程式碼換成數學式如下:
$s = 16 - \lfloor\ \frac{1}{2}(31 - \lfloor log_2 (x - 1) \rfloor)\ \rfloor >= 16 - \frac{1}{2}(31 - log_2 (x - 1))$
$= \frac{1}{2} + log_2 \sqrt{x - 1}$
所以 $2 ^ s >= \sqrt{2} + \sqrt{x - 1} >= \sqrt{2} + \sqrt{x} - 1 > \sqrt{x}$
## 參考資料
[ktvexe 同學共筆](https://hackmd.io/s/BJBZn6Q6)
[baimao8437 同學共筆](https://hackmd.io/s/BkZOVSIcx)
[wrong clock cycle measurements with rdtsc](https://stackoverflow.com/questions/19941588/wrong-clock-cycle-measurements-with-rdtsc)
[C11 - Generic Selections](http://www.robertgamble.net/2012/01/c11-generic-selections.html)
[stackoverflow -- Writing function definition in header files in C++](https://stackoverflow.com/questions/453372/writing-function-definition-in-header-files-in-c)
[what “inline \_\_attribute\_\_((always_inline))” means in the function?](https://stackoverflow.com/questions/22767523/what-inline-attribute-always-inline-means-in-the-function)
[C 語言的各整版本](https://www.crifan.com/summary_c_language_version_c89_amd1_c99_c11/)
[A Fast Hi Precision Fixed Point Divide](http://me.henri.net/fp-div.html)
[ARM Keil](http://www.keil.com/support/man/docs/armclang_asm/armclang_asm_pge1425890373464.htm)
[carolc0708 同學共筆](https://hackmd.io/s/H1EkbFDT)