# 2024q1 課堂教材心得隨筆
contributed by < [`pao0626`](https://github.com/pao0626) >
## [你所不知道的 C 語言:數值系統篇](https://hackmd.io/@sysprog/c-numerics)
### bit-wise 運用:
* 不使用 if-else 分支來處理字元,詳見 ASCII code。
* `('A' | ' ')` 轉成小寫得到 a。
* `('a' & '_')` 轉成小寫得到 A。
* `('a' ^ ' ')` 大小寫互換得到 A。
* swap 實作,節省暫存器使用和記憶體空間。
```c
void xorSwap(int *x, int *y) {
*x ^= *y;
*y ^= *x;
*x ^= *y;
}
```
* Reverse integer bitwise,應用在 LSB <-> MSB 轉換,如開發網路。也可以用於加密。
* XOR 操作可用於圖片加密。
* 巨集 DETECT 偵測 NULL,可以一次比較多個字節,以 32bit 為例
```c
define DETECT(X) (((X) - 0x01010101) & ~(X) & 0x80808080)
```
只有當 $X=0$ 的時候會產生 0x80808080 的結果,只要判斷這個數字是否存在就能找到 ’\0’ 位置。
* 加法實現, `a ^ b` 計算相加但不進位, `(a & b) << 1` 進位但不相加。
其餘可見[你所不知道的 C 語言: bitwise 操作](https://hackmd.io/@sysprog/c-bitwise)。內文提及多種 bitwise 操作,以及兩種位移運算的兩種未定義狀況如下:
* 右移一個負數時,可能是邏輯位移或是算術位移,C 語言標準未定義 (gcc 採算數位移)。
* 左移超過變數長度,其結果未定義;
```cpp
int i=0xFFFFFFFF;
i = i << 32; // 此結果未定義
```
第二種未定義情況在[位元旋轉](https://hackmd.io/@sysprog/bitwise-rotation#%E4%BD%8D%E5%85%83%E6%97%8B%E8%BD%89)中有提到。
```c
static inline __u32 rol32(__u32 word, unsigned int shift) {
return (word << shift) | (word >> (32 - shift));
}
```
當 shift 為 0 時, word >> 32 就會遇到 undefined behavior,因此取代如下:
```c
static inline __u32 rol32(__u32 word, unsigned int shift) {
return (word << shift) | (word >> ((-shift) & 31));
}
```
後續更完善的改進,對 32 位元的 shift 超出 31 時的行為予以調整,採取 mod 操作:
```c
static inline __u32 rol32(__u32 word, unsigned int shift) {
return (word << (shift & 31)) | (word >> ((-shift) & 31));
}
```
需要注意的是,並非所有資料寬度的位元旋轉都要完全遵守上述模式,才可滿足想要的正確性,
考慮 C 語言標準的 integer promotion ,8 bits word 會被提升成 32 bits int 型態,所以可以保持原先寫法:
```c
static inline __u8 rol8(__u8 word, unsigned int shift) {
return (word << shift) | (word >> (8 - shift));
}
```
最後,位元旋轉可被用在 SHA-256 等密碼學上。
### 使用 Count Leading Zero 計算 $log_2 N$。
* gcc 提供 built-in Function 來計算 `clz` ,因此 log2 的程式碼如下:
```c
#define LOG2(X) ((unsigned) \
(8 * sizeof (unsigned long long) - \
__builtin_clzll(X) - 1))
```
其結果等於 8 * 8 - clz - 1,即可得出 64-bit 的 log2 結果。
其中 `sizeof (unsigned long long)` 這個表達式的目的是求得此類型在當前編譯環境中的字節大小。
* `clz` 實作方法有很多種,其中 binary search technique 的方法滿酷的:
```c
int clz(uint32_t x) {
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;
}
```
變數 n 紀錄有幾個 0,以第一個判斷式為例,如果左側 16 bit 皆為 0,將 n 增加 16 後繼續找尋右側 16 bit 還有個 0,反之不做任何處理,往左側找。
* `ffs` 會回傳給定數值的 first bit set 的位置(從右至左)。
## [你所不知道的 C 語言:記憶體管理、對齊及硬體特性](https://hackmd.io/@sysprog/c-memory#%E4%BD%A0%E6%89%80%E4%B8%8D%E7%9F%A5%E9%81%93%E7%9A%84-C-%E8%AA%9E%E8%A8%80%EF%BC%9A%E8%A8%98%E6%86%B6%E9%AB%94%E7%AE%A1%E7%90%86%E3%80%81%E5%B0%8D%E9%BD%8A%E5%8F%8A%E7%A1%AC%E9%AB%94%E7%89%B9%E6%80%A7)
### 背景知識
* 指標篇有提過,`void *` 存在的目的就是為了強迫使用者使用顯式 (explicit) 轉型。
* glibc 提供 `mallinfo()`,`malloc_stats()` 和 `malloc_info()` 函式,可顯示 process 的 heap 資訊,前者返回結構體提供動態記憶體分配訊息,次者不需要参数,也不返回任何值,直接打印資訊。後者需要文件指针。
### 記憶體相關
malloc 給 valid pointer,等真正使用時才知道作業系統是否會 OOM。可比喻成一張支票,能不能拿來開等到兌現才知道。以下順便整理一些常見錯誤:
* Segmentation Fault
定義:發生在進程試圖訪問其記憶體段之外的記憶體,或是無權訪問的記憶體區域時。包括訪問未分配的記憶體、對NULL指針的解引用,或試圖寫入只讀記憶體。
結果:操作系統將中止錯誤的程序,通常伴隨一個核心轉儲 (core dump),有助於開發者後續的調試過程。
* Out of Memory (OOM)
定義:發生在系統無法滿足進程的記憶體分配請求時。在過度分配記憶體或系統可用記憶體耗盡時常見此錯誤。
結果:Linux 等操作系統可能啟動 OOM Killer,選擇並終止占用大量記憶體的進程,以釋放資源並保持系統運行。
* Stack Overflow
定義:發生在進程的呼叫棧 (call stack) 耗盡其為每個線程分配的記憶體空間時。這通常是因為過度的遞歸呼叫或過大的棧分配(如大數組)。
結果:程序也會被操作系統中止。
* 動態配置產生的空間 Heap ,和資料結構的 Heap 無關。
### data alignment
目的: 犧牲記憶體空間,來減少存取次數。
* 使用 `#pragma pack(push, 1)` 和 `#pragma pack(pop)` 來設定範圍內的結構體對齊方式為 1 字節。
* `sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'` 中 `sh -c` 調用 shell 執行一串命令,`echo 3` 清除 pagecache、dentries 及 inodes (1 只清除 pagecache,2 只清除 dentries 和 inodes),而最後的 `/proc/sys/vm/drop_caches` 是 Linux 實時系統信息目錄下的一個可寫入文件。
* Pagecache: 記憶體中的頁(通常 4KB 大小),使用 LRU 更新。
* Dentries: directory entry 的縮寫,用於快取檔案系統的目錄結構訊息,即檔案名稱與inode之間的映射關係,減少對硬碟的讀取操作。
* Inode: 檔案系統中的重要概念,包含檔案的元數據,如檔案的大小、權限、修改時間、資料區塊位置等。每個檔案或目錄在檔案系統中都對應一個唯一的inode。
* 在 64-bit 的系統下,malloc 會以 16 bytes 進行對齊。
* Compare-and-Swap (CAS) 是一種用於多執行緒編程中的原子操作。
- [ ] 包含三个主要的参数:
1. 記憶體位置(Memory Location):進行比較和替換操作的記憶體位址。
2. 期望值(Expected Value):預期在該記憶體位置上找到的值。
3. 新值(New Value):如果記憶體位置的當前值與期望值相匹配,將要儲存的新值。
- [ ] 執行流程:
1. 檢查目標記憶體位置的目前值是否與期望值相等。
2. 如果相等,將記憶體位置的值更新為新值。
3. 無論更新是否成功,都會傳回記憶體位置的原始值(即操作之前的值)。
* 由於記憶體對齊特性,節點的操作中可以將結構體位址的最後一個 bit 標示為是否刪除,以避免 concurrent 下導致的新增刪除錯誤,詳見[內文](https://hackmd.io/vQgqRmTmSBSbR_Wg-wnu8A?view#%E6%A1%88%E4%BE%8B%E5%88%86%E6%9E%90-concurrent-ll)。
### glibc 的 malloc/free 實作
> 額外參考 [glibc malloc 原理分析](https://www.openeuler.org/zh/blog/wangshuo/Glibc%20Malloc%20Principle/Glibc_Malloc_Principle.html)
* Arena (分配區):
是一個結構體,每個執行緒在申請記憶體時會取得一個。分為 main Arena 與 thread Arena,前者僅有一個,其餘皆為thread分配區。當新建立的執行緒需要申請記憶體時,將從一個全域的鍊錶中取得一個空閒的分配區,如果沒有得到且分配區數量沒有超過最大值(M_ARENA_MAX),malloc 將會新建一個。
* Main Arena:第一個被初始化和使用的 arena。首先使用 brk() 系統呼叫從作業系統中取得記憶體。當空間不足會使用 brk() 來延伸其邊界,,預設一次 132 KiB。
* Thread Arena:減少多執行緒環境中對 main arena 的競爭,每個執行緒可能會有自己的 arena。通常透過 mmap() 系統呼叫直接映射內存,預設 map 1 MiB,分配給 heap 132 KiB,尚未分配給 heap 的空間設定為不可讀寫,1 MiB 使用完後會再 map 一個 1 MiB 的新 heap。
* malloc_state (Arena Header):
每個 arena 一個。紀錄 arena 的資訊,包含 bins (free list), top chunk(分裂新 chunk 的地方)等。其中 main arena 的 header 在 data segment(static),thread 的在 heap 中。
* heap_info (Heap Header):
每個 thread arena 可能有超過一個 heap。heap_info 在每個 heap 中紀錄前一個heap、所屬的 arena 等。所以 main arena 沒有,thread arena 可能有多個。
* Chunk(塊):
是記憶體分配中的一個基本單位,是從 heap 中劃分出來。 是 user data 儲存的地方,又依使用狀態分為 free 與 allocated。紀錄 chunk 本身的 size 與型態,以及相鄰的 chunk 資訊。
* Bin:
是紀錄 free chunks 的資料結構(free list)。
* Fast Bin:管理小塊 chunk(64 bytes)的鍊錶。
* Unsorted Bin:fastbin整合的chunk和最近 free 的 small / large chunk,不分大小。
* Small Bin:chunk < 512 bytes。
* Large Bin:chunk >= 512 bytes。
* malloc 流程:
* 調整 malloc size : 加上 overhead 並對齊。
* 從 fastbin 找對應大小。
* 從 smallbin 找對應大小。
* 合併 fastbin 歸入 unsorted bin。
* 從 unsorted bin 找對應大小,若不符合則插入相應的 small / large bin 中。
* 從 large bin 找對應大小。
* 使用 top chunk,若 size 不夠則合併 fastbin,若仍不夠則 system call。
* free 流程:
* 檢查是否為 mmap,若是則 munmap。
* 是否能放入 fastbin,若是則返回。
* 緊鄰 top chunk 則合併。
* 合併相鄰未使用 chunk 後放入 unsorted bin。
* 檢查回收 chunk 是否超過閾值,超過則觸發收縮操作,如果是 main arena 則透過 brk 收縮,否則直接回收整塊 heap 或是收縮 top chunk。
## 課堂討論
float point 如何透過 bit-wise 來乘 2,這需要考慮在C語言中,直接對浮點數的位元模式進行位元操作是不允許的,所以需要先透過指針類型轉換或是 `memcpy` 的方法轉換成 unsigned int 型態。
以下面的打印資訊為例, `40e00000` 的二進制為 `01000000111000000000000000000000`,確實複合 7.0 的 IEEE 754 表示。
```c
#include <stdio.h>
float float_mul2(float x) {
printf("%f\n", x); // 印出 7.00
unsigned int *bits = (unsigned int*)&x;
printf("%x\n", *bits); // 印出 40e00000
*bits += 1 << 23;
printf("%x\n", *bits); // 印出 41600000
return x;
}
int main() {
// Write C code here
float a = 7.0;
printf("%f\n", float_mul2(a)); //印出14.00
return 0;
}
```