# 2017-11-17 課堂練習 - [ ] 2.58: 撰寫函式 `is_little_endian`,在 Little endian 機器上返回 `1`,反之返回 `0`,應能再任意機器上,無論幾位元的架構都適用 (==Page 88==) ```clike=c bool is_little_endian(){ int char_var=1; unsigned char *test = (unsigned char*) &char_var; return test[0]==0; } ``` 已知一台 little endian 的機器,byte order如下 ![byte order](https://chuck42315.files.wordpress.com/2011/10/endianness.jpg) 且對於所有位元來說 unsigned char 的大小較 int 為小,因此當將 int 強制轉為 unsigned char 時會使得最高位為 1 因此我們可以藉由排列順序特性來判斷電腦是 little endian 還是 big endian。 - [ ] 2.66: 撰寫函式 `leftmost_one`,產生得以找出數值 x 最高位 `1` 的位元遮罩 (==Page 90==) ```clike=c int leftmost_one(unsigned x){ x |= x >> 1; x |= x >> 2; x |= x >> 4; x |= x >> 8; x |= x >> 16; return x ^ (x >> 1); //保留最左邊的位元 } ``` 首先該程式碼只能夠在 32 bit 電腦上執行,若要在 64 bit 電腦上執行則需要對應的修改,如: ```clike=c int leftmost_one(unsigned x){ x |= x >> 1; x |= x >> 2; x |= x >> 4; x |= x >> 8; x |= x >> 16; x |= x >> 32; return x ^ (x >> 1); //保留最左邊的位元 } ``` 舉個例子來說,10 進位 `65280` 轉為 2 進位為 `1111 1111 0000 0000`,因此遮罩就是二進位 `1000 0000 0000 0000` 的 `32768`,這題有點類似先前的 clz,我們先來回顧計算 clz 的方法 ```clike=c int clz(uint32_t x) { int n = 32, c = 16; do { uint32_t y = x >> c; if (y) { n -= c; x = y; } c >>= 1; } while (c); return (n - x); } ``` 是以每次 shift 2 的次方位來計算最大的`1`所在的位置,但不同的地方在於 clz 要算的是位數,而本題想要得到的答案為最大`1`和後面無數`0`的數值,因此我們可以透過轉換 `1111 1111 0000 0000` 成 `1111 1111 1111 1111` 的方式再藉由與向右 shift 1 位的`111 1111 1111 1111`做 xor 得到`1000 0000 0000 0000`,因此透過每次向右 shift 2 次方位取 or 的方式轉換結果,code 如上所示。 - [ ] 2.92: 撰寫函式 `float_negate`,給定浮點數 `f`,回傳 `-f`,倘若 `f` 是 $NaN$,直接回傳 `f`。輸入數值範圍為有效的 2^32^ 個數值 (==Page 96==) ```clike=c typedef unsigned float_bits; float_bits float_negate(float_bits f){ unsigned sign = f >> 31; unsigned exponent = f >> 23 & 0xFF; //f >> 23 & 11111111 unsigned significand = f & 0x7FFFFF; /*23bit 111 1111 1111 1111 1111 1111*/ if (exponent == 0xFF && significand != 0) //detect NaN return f; return ~sign << 31 | exponent << 23 | significand; } ``` 本題需要複習 IEEE754 的浮點數表示法,在 IEEE 的標準中,32bit 的浮點數可以表示為 1 bit 的 sign,8 bit 的 exponent 以及 23 bit 的 significand,而完整的浮點數可以由 $(-1)^{sign}*2^{exponent}*significand$ 表示,而另外有幾種特別的表示法: 1. 規格化的值:這個是最常見的表示法,exponent 值不全為 0 也不全為 1,且 significand 須介於 1 <= significand < 2 之間,用於表示一般的浮點數。 2. 非規格化的值:這裡非規格化的值指的是 exponent 全為 0 的情況,通常用於表示 +0.0(sign 為 0) 以及 -0.0(sign 為 1),或是非常接近 0 的數。 3. 而最為特殊的則是 exponent 全為 1 的情況,假如 significand 全為 0 則代表無窮大,反之則表示$NaN$。 有了上述觀念之後,本題的解題方向就很清楚了,題目要求判斷 $NaN$,因此不能夠簡單的把 sign 取 not,而需要判斷 exponent 是否全為 1 即 0xFF 且 significand 是否為 0,這時候就需要將浮點數完全拆開來,最後將浮點數的 3 個部分合在一起輸出即可。 - [ ] 4.47: 以 C 語言指標改寫原本使用陣列表示法的氣泡排序程式碼 (==Page 327==) / 延伸題目: [最佳化](https://en.wikipedia.org/wiki/Bubble_sort#Optimizing_bubble_sort) / [Analysis on Bubble Sort Algorithm Optimization](http://ieeexplore.ieee.org/stamp/stamp.jsp?arnumber=5635119) ```clike=c void bubble_p(long* data, long count) { long *i, *last; for (last = data + count - 1; last > data; last--) { for (i = data; i < last; i++) { if (*(i+1) < *i) { /* swap adjacent elements */ long t = *(i+1); *(i+1) = *i; *i = t; } } } } ``` 由於需要使用指標來當索引,因此最顯著的差別就是一開始 last = data + count - 1 直接將 last 指向該序列的最後一欄,原本的 last > 0 改寫為 last > data,只要指標位置在 data 後則迴圈繼續執行,而其他部分則與原本的類似,只差在使用了 *i 去指向原先的 data 欄位。 - [ ] 6.37: 計算 `N=64` 和 `N=60` 的 cache miss rate (==Page 455==) 首先看 A 的部分,sumA 會先讀取整列,而讀取的單位為 16 byte 即 4 個 int,因此由於 N=64 與 60 都是 4 的倍數,所以每 4 個 int 組為 `miss hit hit hit`所以 sumA 都是 25%。 再來是 sumB,由程式碼中可以看出 A 與 B 差在讀取的順序差異,A 先讀列,B則先讀直行,已知 direct map 的 cache 大小為 4K,因此可以塞得下 4096 個整數,所以以 N=64 來說前 64 次讀取都會是 miss,再來連 3 次 64 直行讀取都會全中,同樣的 N=60 也是 4 的倍數因此也會有相同的過程,故 cache miss 一樣是 25%。 比較麻煩的是 sumC,同時讀取一整塊 2×2 ( a[i][j], a[i+1][j], a[i][j+1], a[i+1][j+1])的整數,因為一次讀 2×2 個,因此 miss rate 為 50%,分別為 `miss miss hit hit` 但因為該迴圈有兩層,故第一層讀完之後,外面那層會全命中,故最終答案為 25%,N=60 時亦同。 該題答案皆為 25%。 - [ ] 6.45: 撰寫更快的轉置矩陣實作 (==Page 458==) 首先使用原先提供的函式,以 perf 執行 100 次得到結果如下 ( dim = 1024 ) ``` Performance counter stats for './test' (100 runs): 39,254 cache-misses # 3.565 % of all cache refs ( +- 5.68% ) 1,101,006 cache-references ( +- 0.01% ) 28,452,308 instructions # 0.60 insn per cycle ( +- 0.00% ) 47,351,420 cycles ( +- 0.24% ) 0.012327468 seconds time elapsed ( +- 1.33% ) ``` ```clike=c void transpose(int* dst, int* src, int dim) { int i, j; for (j = 0; j < dim; j++) for (i = 0; i < dim; i++) dst[j*dim+i] = src[i*dim+j]; } ``` perf 執行 100 次得到的結果如下 ``` Performance counter stats for './test' (100 runs): 5,938 cache-misses # 1.039 % of all cache refs ( +- 13.29% ) 571,488 cache-references ( +- 0.99% ) 28,407,630 instructions # 2.05 insn per cycle ( +- 0.07% ) 13,846,782 cycles ( +- 0.35% ) 0.003801669 seconds time elapsed ( +- 2.03% ) ``` 第一個加速版本只將行列 i, j 進行調換,使得讀取資料彼此接近,locality 發揮作用,大量減少 cache-miss,以 dim = 1024 為例,執行時間由 0.012327468 秒提升為 0.003801669 秒。 ``` ``` 第二個加速版本透過上題的 sumC 啟發,一次處理一整塊 2×2,以下為改寫後的 code ```clike=c void transpose(int* dst, int* src, int dim) { int i, j; for (i = 0; i <= dim-2; i+=2) for (j = 0; j <= dim-2; j+=2) { dst[j*dim+i] = src[i*dim+j]; dst[j*dim+i+1] = src[(i+1)*dim+j]; dst[(j+1)*dim+i] = src[i*dim+j+1]; dst[(j+1)*dim+i+1] = src[(i+1)*dim+j+1]; } } ``` 以 perf 執行 100 次的結果如下,可看出效果比一個一個來還要好,時間提升了 0.007078113 秒 ``` Performance counter stats for './test' (100 runs): 33,621 cache-misses # 5.844 % of all cache refs ( +- 4.48% ) 575,360 cache-references ( +- 0.02% ) 26,068,849 instructions # 1.32 insn per cycle ( +- 0.01% ) 19,722,537 cycles ( +- 0.16% ) 0.005249355 seconds time elapsed ``` 如果將第一個與第二個合在一起實作,可以比單使用第一個加速方法還要好上一點點,cache-miss 甚至少了 26.4% ``` Performance counter stats for './test' (100 runs): 4,371 cache-misses # 1.444 % of all cache refs ( +- 9.98% ) 302,790 cache-references ( +- 0.15% ) 26,063,880 instructions # 2.28 insn per cycle ( +- 0.01% ) 11,421,497 cycles ( +- 0.23% ) 0.003153592 seconds time elapsed ``` 那有沒有可能 4×4 一組會更提升效能呢?以下做了實驗,code 修改如下 ```clike=c void transpose(int* dst, int* src, int dim) { int i, j; for (j = 0; j <= dim-4; j+=4) for (i = 0; i <= dim-4; i+=4) { dst[j*dim+i] = src[i*dim+j]; dst[j*dim+i+1] = src[(i+1)*dim+j]; dst[j*dim+i+2] = src[(i+2)*dim+j]; dst[j*dim+i+3] = src[(i+3)*dim+j]; dst[(j+1)*dim+i] = src[i*dim+j+1]; dst[(j+1)*dim+i+1] = src[(i+1)*dim+j+1]; dst[(j+1)*dim+i+2] = src[(i+2)*dim+j+1]; dst[(j+1)*dim+i+3] = src[(i+3)*dim+j+1]; dst[(j+2)*dim+i] = src[i*dim+j+2]; dst[(j+2)*dim+i+1] = src[(i+1)*dim+j+2]; dst[(j+2)*dim+i+2] = src[(i+2)*dim+j+2]; dst[(j+2)*dim+i+3] = src[(i+3)*dim+j+2]; dst[(j+3)*dim+i] = src[i*dim+j+3]; dst[(j+3)*dim+i+1] = src[(i+1)*dim+j+3]; dst[(j+3)*dim+i+2] = src[(i+2)*dim+j+3]; dst[(j+3)*dim+i+3] = src[(i+3)*dim+j+3]; } } ``` perf 結果如下,發現並沒有提升很多,cache-miss 甚至不減反增,可得一次處理 2×2 是比較好的執行結果 ``` Performance counter stats for './test' (100 runs): 5,386 cache-misses # 3.083 % of all cache refs ( +- 7.56% ) 174,716 cache-references ( +- 0.28% ) 26,652,693 instructions # 2.42 insn per cycle ( +- 0.01% ) 11,031,122 cycles ( +- 0.24% ) 0.003144989 seconds time elapsed ``` - [ ] 6.46: 有向圖轉換為無向圖 (==Page 458==) / 參考: [Graph](http://www.csie.ntnu.edu.tw/~u91029/Graph.html), [相鄰矩陣 (Adjacency Matrix)](https://hellolynn.hpd.io/2017/09/22/%E5%AF%A6%E4%BD%9Cgraph%E8%88%87dfs%E3%80%81bfs%E8%B5%B0%E8%A8%AA/) 本題與上一題類似,不過先來執行原本的程式碼看看結果( dim = 1024 ) ``` Performance counter stats for './test' (100 runs): 11,724 cache-misses # 1.037 % of all cache refs ( +- 14.13% ) 1,130,432 cache-references ( +- 0.01% ) 44,279,814 instructions # 1.49 insn per cycle ( +- 0.00% ) 29,741,080 cycles ( +- 0.84% ) 0.007950646 seconds time elapsed ``` 若我們使用了上題一次處理 2×2 的解法,code 修改如下 ```clike=c void convert(int* src, int dim) { int i, j; for (j = 0; j <= dim-2; j+=2) for (i = 0; i <= dim-2; i+=2){ src[j*dim+i] = src[i*dim+j] || src[j*dim+i]; src[j*dim+i+1] = src[(i+1)*dim+j] || src[j*dim+i+1]; src[(j+1)*dim+i] = src[i*dim+j+1] || src[j+1*dim+i]; src[(j+1)*dim+i+1] = src[(i+1)*dim+j+1] || src[(j+1)*dim+i+1]; } } ``` 可得執行結果如下,得到較好的執行時間 ``` Performance counter stats for './test' (100 runs): 11,903 cache-misses # 2.669 % of all cache refs ( +- 10.73% ) 445,967 cache-references ( +- 0.98% ) 43,205,153 instructions # 2.28 insn per cycle ( +- 0.04% ) 18,931,655 cycles ( +- 1.13% ) 0.005099529 seconds time elapsed ``` ### 參考資料 * [little-endian-vs-big-endian](https://chuck42315.wordpress.com/2011/10/30/little-endian-vs-big-endian/) * CS:APP p.441-442 * CS:APP p.456