延伸問題:
random_shuffle_array
也探討 list_for_each_entry_safe
建構的迴圈中,若將 list_move_tail
換為 list_move
,為何會無法滿足 stable sorting{
struct list_head list_less, list_greater;
struct listitem *pivot;
struct listitem *item = NULL, *is = NULL;
if (list_empty(head) || list_is_singular(head))
return;
INIT_LIST_HEAD(&list_less);
INIT_LIST_HEAD(&list_greater);
pivot = list_first_entry(head, struct listitem, list);
list_del(&pivot->list);
list_for_each_entry_safe (item, is, head, list) {
if (cmpint(&item->i, &pivot->i) < 0)
list_move_tail(&item->list, &list_less);
else
list_move_tail(&item->list, &list_greater);
}
list_quicksort(&list_less);
list_quicksort(&list_greater);
list_add(&pivot->list, head);
list_splice(&list_less, head);
list_splice_tail(&list_greater, head);
}
為什麼改成 list_move
無法 stable sort ?
list_move() - Move a list node to the beginning of the list
list_move_tail() - Move a list node to the end of the list
list_move 後 head->next 會變成 node,list_move_tail 後 head->prev 會變成 node
cmpint return > 0, if 前面參數 > 後面參數
stable sort 要求相同值的元素保持原始相對順序。list_move_tail 確保元素按遍歷順序追加到 list_less 或 list_greater,從而保留原始順序。
舉例來說:
head, 3, 3^, 3', 3" 排序完後應該不變,但若改為 list_move,則排序結果會變成:
pivot: 3
queue: head, 3^, 3', 3"
greater: 3", 3' ,3^
int main(void)
{
struct list_head testlist;
struct listitem *item, *is = NULL;
size_t i;
random_shuffle_array(values, (uint16_t) ARRAY_SIZE(values));
INIT_LIST_HEAD(&testlist);
assert(list_empty(&testlist));
for (i = 0; i < ARRAY_SIZE(values); i++) {
item = (struct listitem *) malloc(sizeof(*item));
assert(item);
item->i = values[i];
list_add_tail(&item->list, &testlist);
}
assert(!list_empty(&testlist));
qsort(values, ARRAY_SIZE(values), sizeof(values[0]), cmpint);
list_quicksort(&testlist);
i = 0;
list_for_each_entry_safe (item, is, &testlist, list) {
assert(item->i == values[i]);
list_del(&item->list);
free(item);
i++;
}
assert(i == ARRAY_SIZE(values));
assert(list_empty(&testlist));
return 0;
}
random_shuffle_array
原始實現使用 get_unsigned16() % (i + 1),這引入了偏誤,因為模運算無法均勻分佈。改進方法是採用 Fisher-Yates (Knuth) 洗牌演算法,並使用更高質量的隨機數生成器:
#define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
static uint16_t values[256];
static inline uint8_t getnum(void)
{
static uint16_t s1 = UINT16_C(2);
static uint16_t s2 = UINT16_C(1);
static uint16_t s3 = UINT16_C(1);
s1 *= UINT16_C(171);
s1 %= UINT16_C(30269);
s2 *= UINT16_C(172);
s2 %= UINT16_C(30307);
s3 *= UINT16_C(170);
s3 %= UINT16_C(30323);
return s1 ^ s2 ^ s3;
}
static uint16_t get_unsigned16(void)
{
uint16_t x = 0;
size_t i;
for (i = 0; i < sizeof(x); i++) {
x <<= 8;
x |= getnum();
}
return x;
}
static inline void random_shuffle_array(uint16_t *operations, uint16_t len)
{
for (uint16_t i = 0; i < len; i++) {
/* WARNING biased shuffling */
uint16_t j = get_unsigned16() % (i + 1);
operations[i] = operations[j];
operations[j] = i;
}
}
在 getnum 內的 s1, s2, s3 不會在每次呼叫該 function 時重置,它們會記住上一次呼叫後的值
C99 (6.2.4.3): An object whose identifier is declared with external or internal linkage, or with the
storage-class specifierstatic
has static storage duration. Its lifetime is the entire
execution of the program and its stored value is initialized only once, prior to program
startup.
static 變數只會在程式開始時初始化一次,而不是每次函數呼叫時都重新初始化。
而 global variable 的 values[256] 也同樣具有靜態儲存期間 ,此外,其他檔案無法透過 extern
訪問
這裡的 getnum 函式其實是一個偽隨機函數 - 線性同餘 ( lcg ),為甚麼要加一個"偽"字是因為這個函數有週期性
LCG 遞迴關係式:
s1 = (171 * s1) % 30269
s2 = (172 * s2) % 30307
s3 = (170 * s3) % 30323
最後 getnum 會截斷 16bit XOR 回傳 8bit結果
get_unsigned16()
: 根據剛剛的邏輯,這個函式是用來生成 16-bit 的隨機數。可以看到使用了 for 迴圈,因為宣告變數 x 為 16-bit 的無符號整數,因此 sizeof(x) = 2 ,經過兩次 for 迴圈,便能得到 16-bit 的隨機數。
random_shuffle_array
: 在 len = 256 的情況下,random_shuffle_array 最終會生成一個包含 0 到 255 的排列,且每個數字恰好出現一次。 然而,可以看到程式中的註解, WARNING biased shuffling ,是因為 get_unsigned16() % (i + 1) 取模的方式會造成某些 index 的選取機率不同。
當 (i+1) 無法整除 get_unsigned16 時,會造成低索引被選擇的機率較高一點點,導致選擇不均勻。
修改如下:
for (uint16_t i = len - 1; i > 0; i--) {
uint16_t j = get_unsigned16() % (i + 1);
uint16_t temp = operations[i];
operations[i] = operations[j];
operations[j] = temp;
}
延伸問題:
clz2 函式的實作如下:
static const int mask[] = {0, 8, 12, 14};
static const int magic[] = {2, 1, 0, 0};
unsigned clz2(uint32_t x, int c)
{
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);
}
用來控制 lower 部分的遮罩位元數,應為 {0, 8, 12, 14},因為每次遞迴將位元數減半 最後兩位使用查表,所以GGGG = 16 - 2 = 14
magic[] 是最後 2 位元的快速查表,應為 {0, 1, 0, 2},對應 00, 01, 10, 11 的 leading zeros。
mask[] 是用來當作 lower 的 mask
當 c=0,x 為 32 bit,所以 lower 為 x 的低16位 (x & 0xFFFF);
當 c=1,x 為 16 bit,所以 lower 為 x 的低8位 (x & 0xFF);
當 c=1,x 為 8 bit,所以 lower 為 x 的低4位 (x & 0xF);
當 c=1,x 為 4 bit,所以 lower 為 x 的低2位 (x & 0x3);
當 c==3 時,表示剩下2 bit,這時我們採用查表的方式確認 leadind zero
magic[0b00] = 2
magic[0b01] = 1
magic[0b10] = 0
magic[0b11] = 0
實作整數開平方根:
uint64_t sqrti(uint64_t x)
{
uint64_t m, y = 0;
if (x <= 1)
return x;
int total_bits = 64;
/* clz64(x) returns the count of leading zeros in x.
* (total_bits - 1 - clz64(x)) gives the index of the highest set bit.
* Rounding that index down to an even number ensures our starting m is a
* power of 4.
*/
int shift = (total_bits - 1 - clz64(x)) & ~1;
m = 1ULL << shift;
根據註解,我們可以得知 m 為 4 的冪,所以要確保 shift 為偶數,也就是說 shift 的 LSB 需為 0 。
因此 MMMM = ~1,任何數 & ~1 即為偶數
至此,m 為 highest set bit
while (m) {
uint64_t b = y + m;
y >>= 1;
if (x >= b) {
x -= b;
y += m;
}
m >>= 2;
}
return y;
}
觀察規律可以發現
假設
若要改為向上取整:
關於 while 迴圈細節可以參考大雞哥詳細解釋
延伸問題:
解釋上述程式碼運作原理,應提供測試程式碼
針對 Linux 核心的 hash table 實作,提供更多圖例並揣摩 Linux 核心開發者
進行《The Art of Computer Programming》(vol 3) 一書section 6.4 的 exercise 8,也就是證明 Theorem S
注意: Linux 核心內部大量使用 hash table,但並非每處的雜湊函數都滿足減少碰撞及避免 clustering 現象的原則,也就是存在若干改進空間
探討 Linux 核心中使用 hash table 的應用案例,至少二項,應當列出原始程式碼並分析,斟酌搭配 git log 以得知 Linux 核心開發者變更程式碼的考量因素