# 2020q3 Homework10 (quiz 10) contributed by < `JimmyLiu0530` > ###### tags: `進階電腦系統理論與實作` ## 測驗1: RGB to Gray 給定每個像素 (pixel) 為 32-bit 的 RGBA 的位元圖 (bitmap),RGBA 代表紅綠藍三原色的首字母,Alpha 值則表示顏色的透明度/不透明度,其轉換為灰階影像 (gray scale) 的函式為: ```c= void rgba_to_bw(uint32_t *bitmap, int width, int height, long stride) { int row, col; uint32_t pixel, r, g, b, a, bw; for (row = 0; row < height; row++) { for (col = 0; col < width; col++) { pixel = bitmap[col + row * stride / 4]; a = (pixel >> 24) & 0xff; r = (pixel >> 16) & 0xff; g = (pixel >> 8) & 0xff; b = pixel & 0xff; bw = (uint32_t) (r * 0.299 + g * 0.587 + b * 0.114); bitmap[col + row * stride / 4] = (a << 24) + (bw << 16) + (bw << 8) + (bw); } } } ``` 人類視網膜包含三類錐形對光敏感的細胞,分別對不同波長範圍的光有反應: ![](https://i.imgur.com/393uLjF.png) > 上圖中的縱軸代表人眼對某對應波長的光的敏感度。其中 1 Å (埃) = $10^{-10}$米 = $0.1nm$ [[出處](http://www.phy.ntnu.edu.tw/demolab/html.php?html=everydayPhysics/color)] 人眼吸收綠色比其他顏色敏感,也可說人眼最容易捕捉到綠色,所以當影像變成灰階時,僅僅將紅色、綠色、藍色加總取平均,不足以反映出人眼所見。常見的方法是將 $Red×77,Green×151,Blue×28$,這三個除數的總和為 `256` (即 $2^8$),可使除法變簡單 (等同於右移 8 位元)。 如果我們將所有的運算都建立表格,於是 ```c bwPixel = table[rgbPixel & 0x00ffffff] + rgbPixel & 0xff000000; ``` $→$ 16 MB; 表格太大 如果先計算針對「乘上 0.299」一類的運算,計算後再建立表格呢? ```c bw = (uint32_t) mul_299[r] + (uint32_t) mul_587[g] + (uint32_t) mul_144[b]; bitmap[col + row * strike / 4] = (a << 24) + (bw << 16) + (bw << 8) + bw; ``` $→$ 降到 32 KB 以內; cache friendly 接著嘗試使用 NEON 指令集加速。將 RGB 三色的權重存入 r3, r4, r5 等暫存器。 `vdup.8` (Vector Duplicate),分別複製到大小為 8 bit 的 NEON register d0 - d2 ```c mov r3, #77 mov r4, #151 mov r5, #28 vdup.8 d0, r3 vdup.8 d1, r4 vdup.8 d2, r5 ``` `vld4.8` (Vector Load),載入 pixel 的資料到 4 個 8-bit 的 NEON register d4-d7,其中那個 4 為 interleave,因為我們有 ARGB,所以 gap = 4。 再來就是計算 weighted average。可用 Vector Multiply 和 Vector Multiply Accumulate 指令: ```c @ (alpha,R,G,B) = (d7,d6,d5,d4) vld4.8 {d4-d7}, [r0]! vmull.u8 q10, d6, d0 vmlal.u8 q10, d5, d1 vmlal.u8 q10, d4, d2 ``` 將值除以 256 就是我們要的灰階值。 `vrshrn` (Vector Shift Right by immediate value) > vrshrn.u16 d4, q10, #8 最後儲存結果。 `vst` (Vector Store) > vst4.8 {d4-d7}, [r3]! 效能評比如下: (對照 `v0` 和 `v4`) ![](https://i.imgur.com/EKG4xuZ.png) 從這張表即可理解,使用 NEON 指令加速只用原本 4.6% 的時間即可完成 RGBA 轉灰階的處理。 [Neon Intrinsics](https://developer.arm.com/architectures/instruction-sets/simd-isas/neon/intrinsics) 允許我們避免直接撰寫組合語言,而是有如 C 函式呼叫,例如 `vmull.u8` 和 `vmlal.u8` 分別對應以下 intrinsics: (提示: 在 Neon Intrinsics 網頁搜尋 NEON intrinsics 可獲得更詳盡的描述) - [vmull_u8](https://developer.arm.com/architectures/instruction-sets/simd-isas/neon/intrinsics?search=vmull_u8) - [vmlal_u8](https://developer.arm.com/architectures/instruction-sets/simd-isas/neon/intrinsics?search=vmlal_u8) 考慮以下使用 `float` 來保存像素的程式碼: ```c= #include <stdint.h> #define MIN(a, b) (((a) < (b)) ? (a) : (b)) #define MAX(a, b) (((a) > (b)) ? (a) : (b)) uint8_t *to_rgb(const float *m, int w, int h, uint8_t *rgb) { int size = w * h; const float *ptr0 = m + (0 * size); const float *ptr1 = m + (1 * size); const float *ptr2 = m + (2 * size); uint8_t *ret = rgb; #define SATURATE_CAST_UCHAR(X) (uint8_t) MIN(MAX((int) (X), 0), 255); for (int remain = size; remain > 0; remain--) { rgb[0] = SATURATE_CAST_UCHAR(*ptr0); rgb[1] = SATURATE_CAST_UCHAR(*ptr1); rgb[2] = SATURATE_CAST_UCHAR(*ptr2); rgb += 3; ptr0++, ptr1++, ptr2++; } #undef SATURATE_CAST_UCHAR return ret; } ``` 函式 `to_rgb` 可將原本透過連續的 `float` 空間保存的資料轉為 RGB 像素,每個像素佔用 3 個位元組,即 24 位元。針對 Aarch64 (Arm 64-bit) 架構 (假設為 little-endian),下方的程式碼則可將 RGB 轉換為灰階: ```c= #include <arm_neon.h> float *rgb_to_gray(const uint8_t *rgb, int w, int h, float *m) { const uint8_t Y_shift = 8; const uint8_t R2Y = 77, G2Y = 151, B2Y = 28; float *ptr = m; int size = w * h; int nn = size >> 3; int remain = size - (nn << 3); uint8x8_t _R2Y = vdup_n_u8(R2Y); uint8x8_t _G2Y = vdup_n_u8(G2Y); uint8x8_t _B2Y = vdup_n_u8(B2Y); for (; nn > 0; nn--) { uint8x8x3_t _rgb = vld3_u8(rgb); uint16x8_t y16 = vmull_u8(_rgb.val[0], _R2Y); y16 = vmlal_u8(y16, _rgb.val[1], _G2Y); y16 = vmlal_u8(y16, _rgb.val[2], _B2Y); y16 = vshrq_n_u16(y16, Y_shift); float32x4_t _ylow = vcvtq_f32_u32(vmovl_u16(vget_low_u16(y16))); float32x4_t _yhigh = vcvtq_f32_u32(vmovl_u16(vget_high_u16(y16))); vst1q_f32(ptr + OFFSET1, _ylow); vst1q_f32(ptr + OFFSET2, _yhigh); rgb += 3 * 8, ptr += 8; } for (; remain > 0; remain--) { *ptr = (rgb[0] * R2Y + rgb[1] * G2Y + rgb[2] * B2Y) >> Y_shift; rgb += 3, ptr++; } return m; } ``` 請補完程式碼,使上述程式碼得以符合預期地運作。 ### 程式說明 line 4-7 初始化等等會用到的變數,像是 RGB 的權重、位移量...等。 因為本題是在 64-bit 的環境下執行,因此暫存器也是 64-bit,代表一輪可以處理 8 個像素。因此, `nn` 紀錄需要做的輪數,剩餘不足一輪的會在 line 33-35 單獨處理。 line 12-14 將 RGB 各顏色的權重分別載入到 64-bit 暫存器,其中載入的方法為複製 8 份 1-byte 權重(e.g. `R2Y`、`G2Y`、`B2Y`)到暫存器中。 每輪執行以下步驟: 1. 先將 8 個像素一次載入到三個暫存器。 ```c uint8x8x3_t _rgb = vld3_u8(rgb); ``` 載入的方式是採 `interleaving`,也就是說從 `rgb` 取到的第一個 byte 會載入到第一個暫存器,取到的第二個 byte 會載入到第二個暫存器,取到的第三個 byte 會載入到第三個暫存器,取到的第四個 byte 會載入到第一個暫存器。 2. 計算出灰階值 ```c uint16x8_t y16 = vmull_u8(_rgb.val[0], _R2Y); y16 = vmlal_u8(y16, _rgb.val[1], _G2Y); y16 = vmlal_u8(y16, _rgb.val[2], _B2Y); y16 = vshrq_n_u16(y16, Y_shift); ``` `vmull_u8` 會將第一個 argument 與第二個 argument 的各個 byte 相乘,因此乘出來的結果就會變成 16 bits。全部的結果就會變成 16 x 8 = 128 bits。`vmulalu8` 就只是 `vmull_u8` 乘完再與另一個暫存器相加而已。 最後,將計算出來的加權值除以 `256`,也就是向右位移 `8` 後,即為灰階值。 3. 存回結果 ```c float32x4_t _ylow = vcvtq_f32_u32(vmovl_u16(vget_low_u16(y16))); float32x4_t _yhigh = vcvtq_f32_u32(vmovl_u16(vget_high_u16(y16))); vst1q_f32(ptr + OFFSET1, _ylow); vst1q_f32(ptr + OFFSET2, _yhigh); rgb += 3 * 8, ptr += 8; ``` 現在要將每個像素的 RGB 從原本 16-bit unsigned 轉成 32-bit float,因為我們原本就是使用 `float` 來保存資料。 轉換的方法如下: - 將原本存於 `y16` 中 8 個像素的資料分成兩群各自去做轉換,因此 `_ylow`、`_yhigh` 裏頭各自有 4 個像素的資料。 - 最後將 `_ylow`、`_yhigh` 存回 `ptr` 中。顯然,`_ylow` 要存回的初始位置就是 `ptr`,所以`OFFSET1 = 0`。因為 `ylow` 佔 32*4 bits,也就是 4 個 float,所以 `_yhigh` 要從 `ptr` + 4 開始存,因此 `OFFSET1 = 4`。 ### 延伸問題 #### 1. 整合 [libattopng](https://github.com/misc0110/libattopng),輸出 RGBA 和灰階的 PNG 圖片 > - 可能會用到 [ImageMagick](https://imagemagick.org/) 事先處理輸入檔案 > - 參照 [Bomb Lab](https://hackmd.io/@posutsai/SyNzl72f4) 實作紀錄 得知如何安裝 GNU Toolchain 及 QEMU ---------------------------------------------- ## 測驗2: LeetCode [1617. Count Subtrees With Max Distance Between Cities](https://leetcode.com/problems/count-subtrees-with-max-distance-between-cities/) 給定 $n$ 個城市,編號從 $1$ 到 $n$,再給予一個大小為 $n-1$ 的陣列 `edges`,其中 $edges[i]=[u_i,v_i]$ 表示城市 $u_i$ 和 $v_i$ 之間存在雙向邊。題目保證任兩個城市間有唯一的路徑,也就是說所有城市構成一棵樹 (tree)。 ![](https://i.imgur.com/q0e7mi2.png) > 上圖的聯通關係為: > - 1, 4 > - 1, 3 > - 3, 2 > - 1, 3, 2 > - 4, 1, 3 > - 4, 1, 3, 2 一棵子樹 (subtree) 是上述所有城市的子集。在子集中,任意城市間可透過子集的其他城市和邊到達。兩個子樹被認為不一樣的條件是,至少有個城市在其中一棵子樹中存在,卻又不存在於另一棵子樹中。 給定 $d$ 範圍介於 $1$ 到 $n-1$,請找出城市間最大距離恰好為 $d$ 的所有子樹數目。請你回傳一個大小為 $n-1$ 的陣列,其中第 $d$ 個元素(下標從 1 開始)是城市間最大距離恰好等於 $d$ 子樹數目。 > 兩個城市間距離定義是,它們之間需要經過的邊的數目。 ![](https://i.imgur.com/n9DqiZE.png) 第一組輸入: - $n=4$ - $edges=[[1,2],[2,3],[2,4]]$ 預期輸出: - $[3,4,0]$ > 既然 $n=4$,於是輸出的陣列會有 3 個元素 > - 子樹 `{1,2}`, `{2,3}` 和 `{2,4}` 最大距離都為 `1` > - 子樹 `{1,2,3}`, `{1,2,4}`, `{2,3,4}` 和 `{1,2,3,4}` 最大距離都為 `2` > - 不存在城市間最大距離為 `3` 的子樹。 第二組輸入: - $n=2$ - $edges=[[1,2]]$ 預期輸出: - $[1]$ > 既然 $n=2$,於是輸出的陣列只會有 1 個元素,且題目已保證任意城市之間只有唯一的路徑 第三組輸入: - $n=3$ - $edges=[[1,2],[2,3]$ 預期輸出: - $[2,1]$ > 既然 $n=3$,於是輸出的陣列會有 2 個元素 > - 子樹 `{1,2}` 和 `{2,3}` 最大距離都為 `1` > - 子樹 `{1,2,3}` 最大距離為 `2` 限制: - $2 \leq n \leq 15$ - $edges.length==n-1$ - $edges[i].length==2$ - $1 \leq u_i,v_i \leq n$ - 題目保證 $(u_i,v_i)$ 所表示的邊互不相同 思路: - 因為 $n \leq 15$,所以就往狀態壓縮 + dynamic programming 的技巧去思考,但本題狀態間的轉移無法進行,只能窮舉所有情況。 - 題目的樹可看成是無向無環連通圖(二者等價) > 在圖論中,環是一條只有第一個和最後一個頂點重複的非空路徑 > - 不具備環的圖被稱作無環圖 > - 不具備有向環的有向圖被稱做有向無環圖 (DAG,directed acyclic graph) > - 無環的連通圖被稱作樹 > ![](https://i.imgur.com/3wPM7aB.png) - 窮舉狀態,首先要判斷是否無效 1. 該狀態僅有 $1$ 個節點,因題目要求的 $d$ 最小是 $1$,即距離為 $1$,所以可縮減 2. 狀態下不是一個子樹,即非一個連通圖,那怎麼判斷是否是連通圖呢?可利用 BFS 或 DFS 判斷所能到達的節點的個數,是否和集合內元素個數 (二進位 $1$ 的個數)相同即可 - 判斷該狀態是有效後,如何求最大距離?所謂的最大距離是無向無環連通圖的最大直徑 以下程式碼是可能的解法: ```c= #include <stdint.h> #include <stdlib.h> #include <string.h> #define MIN(a, b) (((a) < (b)) ? (a) : (b)) #define ITERATE(i, begin, end) \ for (int i = (begin), i##_end_ = (end); i < i##_end_; i++) int *countSubgraphsForEachDiameter(int n, int **edges, int edgesSz, int *edges0sz, int *rsz) { uint8_t d[n][n]; /* distance */ memset(d, 0xFF, sizeof(d)); uint16_t conn[n]; ITERATE (i, 0, n) conn[i] = 1 << i; for (int z = 0; z < edgesSz; z++) { int *e = edges[z]; int u = e[0] - 1, v = e[1] - 1; d[u][v] = d[v][u] = 1; conn[u] |= 1 << v, conn[v] |= 1 << u; } ITERATE (k, 0, n) ITERATE (i, 0, n) ITERATE (j, 0, n) d[i][j] = MIN(d[i][j], d[i][k] + d[k][j]); int *rv = calloc(n - 1, sizeof(int)); ITERATE (S, 1, (1 << n)) { int ctmp = 1 << __builtin_ctz(S); while (1) { int conn_nxt = ctmp; ITERATE (d, 0, n) if ((ctmp >> d) & 1) conn_nxt |= conn[d]; conn_nxt &= S; if (COND1) break; ctmp = conn_nxt; } if (COND2) continue; uint8_t ret = 0; ITERATE (i, 0, n) if ((S >> i) & 1) ITERATE (j, 0, i) if (((S >> j) & 1) && (ret < d[i][j])) ret = d[i][j]; if (!ret) continue; rv[ITER]++; } *rsz = n - 1; return rv; } ``` 參考資訊: - [花花醬解說: Count Subtrees With Max Distance Between Cities](https://www.youtube.com/watch?v=qag7m9VRf-A&feature=youtu.be) - [Bit manipulation and diameter of a tree](https://jimmy-shen.medium.com/bit-manipulation-and-diameter-of-a-tree-3856d549c5f8) ### 程式說明 首先,介紹一些變數的意義: - `d[i][j]`:節點 `i` 與節點 `j` 之間的最短距離 - `conn[i]`: 與節點 `i` 相鄰的點,用二進位表示 根據 input,將 `conn`、`d` 做初始化,然後計算兩點間的最短距離。 上面提到,我們採用窮舉所有子樹來求解,不過我們要先去檢查該子樹是否為連通? 作法說明如下: `S` 代表一顆子樹,從這子樹中 index 最小的點開始,不斷新增與此點相鄰的點進 `conn_nxt`,因為我們只在乎這顆子樹的點而已,所以其他不在這子樹的點我們都忽略,對應到程式碼 ```c conn_nxt &= S; ``` 透過不斷的從現有的點中往外擴展,直到收斂,因此,`COND1 為 conn_nxt == ctmp`。 此時,若該子樹為連通,則 `ctmp` 一定包含該子樹所有的點。反之,如果 `ctmp` 沒有包含該子樹所有的點,也就是說 `~ctmp & S != 0`,則該子樹不連通,因此 `COND2 == ~ctmp & S`。 接著確認是連通圖後,針對該子樹去找出這棵樹的 diameter,即 $max_{u,v∈V} \ d[u][v]$,其中 $V$ 為該子樹的點集合。找法說明如下: 看過所有該子圖中的點與子圖中其他點的距離,就可以找出最大的距離,即 diameter。 最後,根據該子樹的 diameter 去遞增相對應 `rv[]` 的位置,因此,`ITER == --ret`。 - [ ] 指出改進方式 - 更有效率找 all pair shortest path 的方法-[BFS](https://mathoverflow.net/questions/59680/all-pairs-shortest-paths-in-trees): $O(n^3)$ to $O(n^2)$ ### 延伸問題 #### 1. 用 C99/C11 (可搭配 GNU extension) 重新實作符合 LeetCode 要求但效率優於上述表現的實作,一併分析時間複雜度。