# 2025q1 Homework5 (assessment) contributed by < `gnkuan0712` > ## 一對一檢討 ### 2025/05/09: bitwise, IEEE 754 #### 題目 實作`geomean`,計算幾何平均數,不能用 **FPU** ```c int geomean(float *in, int n) { } // n 表示 in 的總數 ``` #### 補足背景知識 首先根據 `IEEE 754` 單精度浮點數會有 32 bits,其編碼方式為: $$value = (-1)^s \times M \times 2^E$$ ![image](https://hackmd.io/_uploads/HywVWZmZll.png) * fraction(M)部分存放小數點後資料 * 舉例: `8.5 = 2³ + 1/2¹` 轉成二進制為 `1000.1` 調整成 `1.0001 x 2³` * E的部分就是3,但exp編碼以`01111111 (127)`表示指數為0,所以指數為3時應先+3,表示成`10000010 (130)` * frac(M) 的部分就是`0001`因此放到mantissa裡就是`00010000000000000000000` (共23bits) * 把`uint32_t a`的三個部分取出方法 ```c uint32_t sign_a = a >> 31; uint32_t exp_a = (a >> 23) & 0xFF; //0xFF = 1111 1111₂ 把以外的位元都清0 uint32_t frac_a = a & 0x7FFFFF; ``` 接著兩個單精度浮點數相乘a*b $$value = (-1)^{s_a+s_b} \times (M_a \times M_b) \times 2^{E_a+E_b}$$ * sign的部分其實就是把${s_a和s_b}$做XOR * 接下來M的部分因為一開始在存放時,已省略1.0001最前面的1了,所以在做乘法時應當要補回 mantissa 最前面的1: `M_a = (1 << 23) | frac_a; ` 再做相乘 * exp的部分就是直接相加 除了以上大概念外,還會遇到像是 mantissa 相乘時會有 overflow 和正規化的問題的問題 * IEEE‑754 要求 mantissa 必須是 `1.xxxx` 的形式,也就是在 1.0~2.0 之間。如果乘出來 ≥ 2.0,就要把尾數右移一位,並把 exponent 加 1。 * 參考[你所不知道的 C 語言: 浮點數運算](https://hackmd.io/@sysprog/c-floating-point)中有提到浮點數的加法,若超過1也是把其中一個 mantissa 往右移,然後 exp 加一。 ![image](https://hackmd.io/_uploads/rkGMgH4Wll.png) * 所以乘法應當也會有兩種情況 | 乘積大小 | Bit47 | 代表值範圍 | 處理方式 | | ---------- | ----- | ----------- | ----------------- | | ≥ 2.0 | 1 | \[2.0, 4.0) | `P >>= 24; exp++` | | \[1.0,2.0) | 0 | \[1.0, 2.0) | `P >>= 23;` | ```c uint64_t P = M_a * M_b; // 正規化:若 ≥2.0,右移 24, exp+1;否則右移 23 if (P & (1ULL<<47)) { //假設Bit47是1則代表乘積超過2 P >>= 24; exp_r += 1; } else { P >>= 23; } ``` 最後我認為若要擴展到多個浮點數相乘,應該要一邊做正規化,保持 mantissa 在 23 bits,但這樣會一直遇到**精準度遺失**的問題。 ==TODO: 如何最大保留精準度== 原本上面的作法是無條件捨取,所以誤差會很顯著,接著參考了 [CMU: computer system](https://www.cs.cmu.edu/~213/lectures/m22/04-float.pdf) 中 page 30 有提到可以採用 round 的方式,也就是對 `mantissa` 做四捨五入。 四捨五入的實作用 bitwise 操作可以是 P 加上 P 的一半再往右移 n 個 bits `P = (P + (1ULL << (n - 1))) >> n;` ## 我的學習狀況 ### 上課狀況 > 在第11周的隨堂測驗有提到「不管大小寫字母比較」的實做,這是我上課就聽的懂的程式碼 實作流程: * 因為ASCII碼大小寫差32,所以只要往左移5,強迫它變成小寫 * ((ca - 'A') <= ('Z' - 'A'))) 看是不是大寫 * 是:回傳1 * 否:回傳0 * 接著就把1往左移5變成10000 * 然後再跟原本做or所以就會強制轉小寫 心得: 我認為這個寫法真的超酷的,尤其判斷大小寫不管是哪一個都是做一樣的操作(往左移五個bit),不用再做額外不同的操作 > ```c static inline bool ci_ascii_eq(const char *a, const char *b, size_t n) { while (n--) { unsigned char ca = *(const unsigned char *) a++; unsigned char cb = *(const unsigned char *) b++; ca |= ((unsigned char) ((ca - 'A') <= ('Z' - 'A'))) << 5; cb |= ((unsigned char) ((cb - 'A') <= ('Z' - 'A'))) << 5; if (ca != cb) return false; } return true; } ``` ### 作業狀況 * [hw1](https://hackmd.io/ukV-iqKuS4e6-ysqDtpQLA?view) * 實作 queue.[ch] * 實作 shuffle 命令 * [hw3](https://hackmd.io/@kirua/linux2025-homework3) * 把畫面呈現這部份從 kernal space 移到 kernal space ## [因為自動飲料機而延畢的那一年](https://www.opasschang.com/docs/the-story-of-auto-beverage-machine-1):閱讀啟發 我看到了不斷在試錯,然後做實驗,透過數據來調整下一步方向的開發過程。 ## 研讀第 1 到第 6 週「[課程教材](https://wiki.csie.ncku.edu.tw/sysprog/schedule)」和 [CS:APP 3/e](https://csapp.cs.cmu.edu/) (至少到第二章) ### 第一章 1. DMA ## 專題的想法 > 參考[2023](https://hackmd.io/@sysprog/linux2023-projects)、[2024](https://hackmd.io/@sysprog/linux2024-showcase) * [回顧〈並行和多執行緒程式設計〉](https://hackmd.io/@sysprog/linux2023-projects#%E4%B8%A6%E8%A1%8C%E7%A8%8B%E5%BC%8F%E8%A8%AD%E8%A8%88) * 這個我有興趣回顧 * [高效網頁伺服器](https://hackmd.io/@sysprog/linux2023-projects#%E9%AB%98%E6%95%88%E7%B6%B2%E9%A0%81%E4%BC%BA%E6%9C%8D%E5%99%A8) * 我想要研讀、撰寫document: [參考此同學](https://hackmd.io/@sysprog/HJgX4_MH3) * [透過 Netfilter 自動過濾廣告](https://hackmd.io/@sysprog/linux2023-projects#%E9%80%8F%E9%81%8E-Netfilter-%E8%87%AA%E5%8B%95%E9%81%8E%E6%BF%BE%E5%BB%A3%E5%91%8A) * 我覺得這個很酷 ### Backup [1 on 1](https://hackmd.io/@sysprog/HkNNMsjaJl) ### 2的幾次方 ```c #include <stdio.h> #include <stdint.h> #include <string.h> float my_ldexpf(float x, int n){ union{ float f; uint32_t u32; }val; val.f = x; uint32_t sign = val.u32 & 0x80000000; uint32_t exp = (val.u32 >> 23) & 0xff; uint32_t frac = val.u32 & 0x7fffff; // x = +-inf or 0 or n=0 if (exp >= 0xff || exp == 0 || n ==0) return x; uint32_t new_exp = exp + n; val.u32 = sign | new_exp << 23 | frac; return val.f; } int main() { printf("%f", my_ldexpf(2.0, 2)); return 0; } ``` ### 兩數相乘 ```c #include <stdio.h> #include <stdint.h> #include <string.h> #include <math.h> // 把 float 轉成 uint32_t bits static inline uint32_t float_to_bits(float f) { uint32_t u; memcpy(&u, &f, sizeof(u)); return u; } // 把 uint32_t bits 轉回 float static inline float bits_to_float(uint32_t u) { float f; memcpy(&f, &u, sizeof(f)); return f; } // 純 bitwise 模擬單精度浮點乘法 float multiply(float a, float b) { uint32_t A = float_to_bits(a); uint32_t B = float_to_bits(b); // 拆出 sign / exp / frac uint32_t sign_a = A >> 31; uint32_t exp_a = (A >> 23) & 0xFF; uint32_t frc_a = A & 0x7FFFFF; uint32_t sign_b = B >> 31; uint32_t exp_b = (B >> 23) & 0xFF; uint32_t frc_b = B & 0x7FFFFF; // 結果 sign = xor uint32_t sign_r = sign_a ^ sign_b; // 重建 24‑bit mantissa(加回隱藏的 1) uint64_t M_a = (1ULL<<23) | frc_a; uint64_t M_b = (1ULL<<23) | frc_b; // 整數乘法 → 48-bit uint64_t P = M_a * M_b; // 新 exponent = exp_a + exp_b - bias(127) int32_t exp_r = (int32_t)exp_a + (int32_t)exp_b - 127; // 正規化:若 ≥2.0,右移 24, exp+1;否則右移 23 if (P & (1ULL<<47)) { P >>= 24; exp_r += 1; } else { P >>= 23; } // 取出 23-bit fraction uint32_t frc_r = (uint32_t)(P & 0x7FFFFF); // 處理 under/overflow if (exp_r <= 0) return sign_r ? -0.0f : 0.0f; if (exp_r >= 255) return sign_r ? -INFINITY : INFINITY; // 重組 bits uint32_t R = (sign_r << 31) | ((exp_r & 0xFF) << 23) | frc_r; return bits_to_float(R); } int main(void) { float x, y, z; // 測試 1 x = 2.0f; y = 3.5f; z = multiply(x, y); printf("%f * %f = %f\n", x, y, z); return 0; } ``` ### GeoMean ```c #include <stdint.h> #include <string.h> // Q16.16 固定小數常數 #define Q16_ONE (1<<16) // 1.0 in Q16.16 #define LOG2_K 94548 // ≈ (1/ln2)*2^16 #define EXP2_K 45426 // ≈ ln2*2^16 // 將 float bit 轉成 uint32 static inline uint32_t float_to_bits(float f) { uint32_t u; memcpy(&u, &f, sizeof(u)); return u; } // 將 uint32 bit 轉回 float static inline float bits_to_float(uint32_t u) { float f; memcpy(&f, &u, sizeof(f)); return f; } int geomean(float *in, int n) { int64_t sum_log2 = 0; // Q16.16 下的累加 for (int i = 0; i < n; i++) { uint32_t v = float_to_bits(in[i]); int32_t exp = ((v >> 23) & 0xFF) - 127; // 真實指數 E uint32_t frac = v & 0x7FFFFF; // fraction bits // 將 mantissa 轉成 Q16.16: f = mant/2^23 → mant>>7 int32_t f_fp = (int32_t)(frac >> 7); // log2(x) ≈ E + f*(1/ln2) int32_t log2i = (exp << 16) + (int32_t)(((int64_t)f_fp * LOG2_K) >> 16); sum_log2 += log2i; } // 平均後的 log2 (Q16.16) int32_t avg_log2 = (int32_t)(sum_log2 / n); // 分離整數、小數部分 int32_t I = avg_log2 >> 16; int32_t f_fp = avg_log2 & 0xFFFF; // exp2(f) ≈ 1 + f*ln2 → Q16.16 int32_t mant_fp = Q16_ONE + (int32_t)(((int64_t)f_fp * EXP2_K) >> 16); // 重組 IEEE‑754 bits int32_t exp_out = I + 127; if (exp_out <= 0) return 0; // underflow → 0 if (exp_out >= 255) return 0x7F800000; // overflow → +∞ uint32_t frac_out = (uint32_t)(((int64_t)(mant_fp - Q16_ONE) * (1<<23)) >> 16) & 0x7FFFFF; uint32_t result = ((uint32_t)exp_out << 23) | frac_out; // 轉回 float 並四捨五入成 int float gf = bits_to_float(result); return (int)(gf + 0.5f); } int main(void) { float data1[] = {2.0f, 8.0f, 4.0f}; int gm1 = geomean(data1, 3); printf("Test 1: geomean({2,8,4}) = %d (預期約 4)\n", gm1); } ```